[英]Why does DoubleStream.sum()'s result differ from straight addition?
[英]Is Java-8's DoubleStream.sum() method stable when run in parallel?
我對Java 8中的以下構造感到好奇:
double[] doubles = //...
double sum = DoubleStream.of(doubles).parallel().sum();
切入追逐:
sum
的值總是相同,例如在不同的計算機上運行時? 更多背景......
浮點運算是有損的,並且(與實值運算不同)不是關聯的。 因此,除非注意工作如何分割和重新組合,否則可能導致不確定的結果。
我很高興地發現sum()
方法在引擎蓋下使用了Kahan Summation 。 這顯着減少了錯誤,但仍未提供精確的*結果。
在我的測試中,重復調用似乎每次返回相同的結果,但我想知道我們可以安全地假設它是多么穩定。 例如:
我很高興在每台計算機上采用相同的JVM版本。
這是我掀起的一項測試:
public static void main(String[] args) {
Random random = new Random(42L);
for (int j = 1; j < 20; j++) {
// Stream increases in size and the magnitude of the values at each iteration.
double[] doubles = generate(random, j*100, j);
// Like a simple for loop
double sum1 = DoubleStream.of(doubles).reduce(0, Double::sum);
double sum2 = DoubleStream.of(doubles).sum();
double sum3 = DoubleStream.of(doubles).parallel().sum();
System.out.println(printStats(doubles, sum1, sum2, sum3));
// Is the parallel computation stable?
for (int i = 0; i < 1000; i++) {
double sum4 = DoubleStream.of(doubles).parallel().sum();
assert sum4 == sum3;
}
Arrays.sort(doubles);
}
}
/**
* @param spread When odd, returns a mix of +ve and -ve numbers.
* When even, returns only +ve numbers.
* Higher values cause a wider spread of magnitudes in the returned values.
* Must not be negative.
*/
private static double[] generate(Random random, int count, int spread) {
return random.doubles(count).map(x -> Math.pow(4*x-2, spread)).toArray();
}
private static String printStats(double[] doubles, double sum1, double sum2, double sum3) {
DoubleSummaryStatistics stats = DoubleStream.of(doubles).summaryStatistics();
return String.format("-----%nMin: %g, Max: %g, Average: %g%n"
+ "Serial difference: %g%n"
+ "Parallel difference: %g",
stats.getMin(), stats.getMax(), stats.getAverage(), sum2-sum1, sum3-sum1);
}
當我運行它時,前幾次迭代是:
-----
Min: -1.89188, Max: 1.90414, Average: 0.0541140
Serial difference: -2.66454e-15
Parallel difference: -2.66454e-15
-----
Min: 0.000113827, Max: 3.99513, Average: 1.17402
Serial difference: 1.70530e-13
Parallel difference: 1.42109e-13
-----
Min: -7.95673, Max: 7.87757, Average: 0.0658356
Serial difference: 0.00000
Parallel difference: -7.10543e-15
-----
Min: 2.53794e-09, Max: 15.8122, Average: 2.96504
Serial difference: -4.54747e-13
Parallel difference: -6.82121e-13
請注意,雖然sum2
和sum3
可以假設比sum1
更准確 - 但它們可能彼此不一樣!
我用42種種子Random
播種,所以如果有人得到不同的結果,那就會立刻證明一些東西。 :-)
*
對於好奇......
我認為DoubleStream :: sum的文檔非常清楚這個問題:
[..]浮點和的值是輸入值以及加法運算順序的函數。 故意不定義該方法的加法運算的順序以允許實現靈活性以提高計算結果的速度和准確性。 [..]
這意味着,您不應該依賴穩定性,特別是不要依賴於並行流。
另一方面,每次運行都會看到相同的結果,這並不奇怪。 從概念上講 , sum方法可以實現如下:
double sum(double[] array, int startInclusive, int endExclusive) {
int distance = endExclusive - startInclusive;
if (distance < 1000) {
double total = 0;
for (int i = startInclusive; i < endExclusive; ++i) {
total += array[i];
}
return total;
} else {
int middle = startInclusive + distance / 2;
var left = async sum(array, startInclusive, middle);
var right = async sum(array, middle, endExclusive);
return await left + await right;
}
}
雖然異步執行任務的調度是非確定的,但該方法總是返回相同的結果,因為加法運算的順序是相同的(即括號不重新排列 )。
但是,更復雜的實現可能會考慮當前的工作負載以及子任務的預期執行時間(與異步操作的成本相比)。 如果發生這種情況,結果可能會有所不同
我確實得到了與你發布的並行求和結果不同的結果,所以我可以確認它在所有情況下都不穩定。 在您的測試和測試中,序列求和似乎表現相同。 我的JVM可能與您的JVM不同,我可能擁有與您相同數量的內核。 無論如何,這是我為您發布結果的相同迭代獲得的結果。
Oracle Corporation
Java HotSpot(TM) 64-Bit Server VM
25.51-b03
-----
Min: -1.89188, Max: 1.90414, Average: 0.0541140
Serial difference: -2.66454e-15
Parallel difference: -2.66454e-15
-----
Min: 0.000113827, Max: 3.99513, Average: 1.17402
Serial difference: 1.70530e-13
Parallel difference: 1.70530e-13
-----
Min: -7.95673, Max: 7.87757, Average: 0.0658356
Serial difference: 0.00000
Parallel difference: 3.55271e-15
-----
Min: 2.53794e-09, Max: 15.8122, Average: 2.96504
Serial difference: -4.54747e-13
Parallel difference: -4.54747e-13
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.