嵌套 Java 8 并行 forEach 循环性能较差。此行为是预期的吗?
注意:我已经在另一篇 SO 文章中解决了这个问题 - 在嵌套的 Java 8 并行流操作中使用信号量可能会死锁。这是一个错误吗?-,但是这篇文章的标题表明问题与信号量的使用有关-这在某种程度上分散了讨论的注意力。我创建这个是为了强调嵌套循环可能存在性能问题 - 尽管这两个问题都有一个共同的原因(也许是因为我花了很多时间来解决这个问题)。(我不认为它是重复的,因为它正在强调另一个症状 - 但如果你只是删除它)。
问题:如果嵌套两个 Java 8 stream.parallel().forEach 循环,并且所有任务都是独立的、无状态的等任务(除了提交到公共 FJ 池之外),那么在并行循环中嵌套并行循环的性能比在并行循环中嵌套顺序循环要差得多。更糟糕的是:如果包含内部循环的操作是同步的,你会得到一个死锁。
性能问题的演示
如果没有“已同步”,您仍然可以观察到性能问题。您可以在以下位置找到这方面的演示代码:http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachTest.java(有关更详细的说明,请参阅那里的JavaDoc)。
我们在这里的设置如下:我们有一个嵌套的 stream.parallel().forEach()。
- 内部环路是独立的(无状态,无干扰等 - 除了使用公共池),并且在最坏的情况下总共消耗1秒,即如果按顺序处理。
- 外部循环的一半任务在该循环之前消耗 10 秒。
- 一半在循环后消耗10秒。
- 因此,每个线程总共消耗 11 秒(最坏情况)。* 我们有一个布尔值,它允许将内部循环从 parallel() 切换到 sequential()。
现在:将 24 个外循环任务提交到并行度为 8 的池中,我们最多只能 24/8 * 11 = 33 秒(在 8 核或更好的计算机上)。
结果是:
- 使用内部顺序循环:33 秒。
- 内部并行循环:>80秒(我有92秒)。
问题:您能确认此行为吗?这是人们对框架的期望吗?(我现在更小心了,声称这是一个错误,但我个人认为这是由于ForkJoinTask实现中的错误。备注:我已将此发布到并发利益(请参阅 http://cs.oswego.edu/pipermail/concurrency-interest/2014-May/012652.html),但到目前为止,我还没有从那里得到确认)。
僵局的证明
下面的代码将死锁
// Outer loop
IntStream.range(0,numberOfTasksInOuterLoop).parallel().forEach(i -> {
doWork();
synchronized(this) {
// Inner loop
IntStream.range(0,numberOfTasksInInnerLoop).parallel().forEach(j -> {
doWork();
});
}
});
其中 , , 和 是一些无状态的 CPU 刻录机。numberOfTasksInOuterLoop = 24
numberOfTasksInInnerLoop = 240
outerLoopOverheadFactor = 10000
doWork
您可以在 http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachAndSynchronization.java 找到完整的演示代码(有关更详细的说明,请参阅那里的JavaDoc)。
此行为是预期的吗?请注意,有关 Java 并行流的文档没有提到嵌套或同步的任何问题。此外,没有提到两者都使用公共分叉连接池的事实。
更新
有关性能问题的另一个测试可以在 http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachBenchmark.java 中找到 - 此测试没有任何阻塞操作(没有Thread.sleep并且未同步)。我在这里整理了一些评论:http://christian-fries.de/blog/files/2014-nested-java-8-parallel-foreach.html
更新 2
看起来这个问题和带有信号量的更严重的死锁已经在Java8 u40中得到了修复。