【问题标题】:Is Java-8's DoubleStream.sum() method stable when run in parallel?Java-8 的 DoubleStream.sum() 方法在并行运行时是否稳定?
【发布时间】:2014-06-28 03:56:44
【问题描述】:

我很好奇 Java 8 中的以下构造:

double[] doubles = //...
double sum = DoubleStream.of(doubles).parallel().sum();

切入正题:

  • sum 的值是否始终相同,例如什么时候在不同的计算机上运行?

更多背景...

浮点算术是有损的并且(与实值算术不同)不是关联的。因此,除非注意工作的划分和重组方式,否则可能会导致不确定的结果。

我很高兴地发现 sum() 方法在底层使用了 Kahan Summation。这显着减少了错误,但仍然不能给出精确的*结果。

在我的测试中,重复调用似乎每次都返回相同的结果,但我想知道我们可以安全地假设它有多稳定。例如:

  1. 在所有情况下都稳定吗?
  2. 在具有相同内核数的计算机上是否稳定?
  3. 仅在给定计算机上稳定?
  4. 不能完全依赖它的稳定性吗?

我很高兴假设每台计算机上的 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

请注意,虽然 sum2sum3 可以被认为比 sum1 更准确 - 它们可能彼此不同!

我将Random 播种为 42,所以如果有人得到与我不同的结果,那将立即证明一些事情。 :-)


* 对于好奇的人......

  • 这里是some (python) algorithms,可以提供精确的结果
  • 具有我听说过的最佳性能特征的精确和算法是given here(ACM 订阅或需要付费)。每个输入需要 5 次触发器,但是(用 C 语言编写)是为了利用指令级并行性,并且只比简单求和慢 2 到 3 倍,这对于精确的结果来说听起来相当不错。 (c.f. Kahan summation at 4 flops per input)

【问题讨论】:

  • +1 表示一个写得很好的、有趣的问题,其中包含一个测试用例! (现在很少有这样的问题......)
  • 我希望答案是“不,根本不要指望稳定。”
  • 我认为DoubleStream::sum 的文档非常清楚地说明了这个问题:“浮点和的值是输入值和顺序的函数 加法运算。有意不定义此方法的加法运算顺序,以允许实现灵活性以提高计算结果的速度和准确性。"
  • @nosid 是的,我认为这就是它的全部内容,并且很乐意接受作为答案发布的内容。我应该更仔细地重新阅读那段 :-) 纯粹出于我的好奇心,如果有人能对当前的实现再多说一点,我仍然很感兴趣。是什么让它在我的测试中稳定? (我实际上另有预期)。它在多 CPU 架构上会稳定吗? (我只是多核。)
  • 您最大的数据集是 2k 浮点数,假设实现相当聪明,它不会为该大小启动任何线程。同步等(即使假设现有线程池)会比我认为的单线程实现更昂贵(8 dp flops 意味着如果最佳,我们将在这里花费

标签: java multithreading java-8 numeric java-stream


【解决方案1】:

我认为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;
    }
}

虽然异步执行任务的调度是不确定的,但该方法总是返回相同的结果,因为加法操作的顺序是相同的(即括号没有重新排列)。

但是,更复杂的实现可能会考虑当前工作负载以及子任务的预期执行时间(与异步操作的成本相比)。如果发生这种情况,结果可能会有所不同。

【讨论】:

    【解决方案2】:

    我确实得到了与您发布的并行求和不同的结果,因此我可以确认它在所有情况下都不稳定。在您的测试和我的测试中,串行求和的行为似乎相同。我的 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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-11-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-26
      • 2018-05-28
      相关资源
      最近更新 更多