使用大型并行Java 8流时如何防止堆空间错误

时间:2015-02-13 09:23:23

标签: java parallel-processing java-8 java-stream

如何有效地并行计算pi的计算(仅作为示例)?

这可行(并且在我的机器上大约需要15秒):

Stream.iterate(1d, d->-(d+2*(Math.abs(d)/d))).limit(999999999L).mapToDouble(d->4.0d/d).sum()

但以下所有并行变体都会遇到OutOfMemoryError

DoubleStream.iterate(1d, d->-(d+2*(Math.abs(d)/d))).parallel().limit(999999999L).map(d->4.0d/d).sum();
DoubleStream.iterate(1d, d->-(d+2*(Math.abs(d)/d))).limit(999999999L).parallel().map(d->4.0d/d).sum();
DoubleStream.iterate(1d, d->-(d+2*(Math.abs(d)/d))).limit(999999999L).map(d->4.0d/d).parallel().sum();

那么,我需要做些什么才能并行处理这个(大)流? 我已经检查过autoboxing是否导致内存消耗,但事实并非如此。这也有效:

DoubleStream.iterate(1, d->-(d+Math.abs(2*d)/d)).boxed().limit(999999999L).mapToDouble(d->4/d).sum()

2 个答案:

答案 0 :(得分:3)

问题在于您使用的是难以并行化的构造。

首先,Stream.iterate(…)创建一个数字序列,其中每个计算都取决于先前的值,因此,它没有提供并行计算的空间。更糟糕的是,它创建了一个无限的流,它将由实现处理,就像一个未知大小的流。对于拆分流,必须先将值收集到数组中,然后才能将它们移交给其他计算线程。

其次,提供limit(…)不会改善情况,it makes the situation even worse。应用限制会删除实现刚为阵列片段收集的大小信息。原因是流是有序,因此处理数组片段的线程不知道它是否可以处理所有元素,因为这取决于其他线程正在处理的先前元素的数量。这是documented

  

“...在有序的并行管道上可能相当昂贵,特别是对于maxSize的大值,因为limit(n)被限制为不仅返回任何 n 元素,而是遇到订单中的前n个元素。“

我们完全知道iterate返回的无限序列与limit(…)的组合实际上具有完全已知的大小,这真是遗憾。但实施不知道。并且API没有提供创建两者的有效组合的方法。但我们可以自己做:

static DoubleStream iterate(double seed, DoubleUnaryOperator f, long limit) {
  return StreamSupport.doubleStream(new Spliterators.AbstractDoubleSpliterator(limit,
     Spliterator.ORDERED|Spliterator.SIZED|Spliterator.IMMUTABLE|Spliterator.NONNULL) {
       long remaining=limit;
       double value=seed;
       public boolean tryAdvance(DoubleConsumer action) {
           if(remaining==0) return false;
           double d=value;
           if(--remaining>0) value=f.applyAsDouble(d);
           action.accept(d);
           return true;
       }
   }, false);
}

一旦我们有了这样的迭代限制方法,我们可以像

一样使用它
iterate(1d, d -> -(d+2*(Math.abs(d)/d)), 999999999L).parallel().map(d->4.0d/d).sum()
由于源的顺序性,这仍然不会从并行执行中获益,但它可以工作。在我的四核机器上,它获得了大约20%的收益。

答案 1 :(得分:0)

这是因为ForkJoinPool方法使用的默认parallel()实现不会限制创建的线程数。解决方案是提供ForkJoinPool的自定义实现,该实现仅限于并行执行的线程数。这可以通过以下方式实现:



ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
forkJoinPool.submit(() -> DoubleStream.iterate(1d, d->-(d+2*(Math.abs(d)/d))).parallel().limit(999999999L).map(d->4.0d/d).sum());