Java Fork-Join(Java 8)中出现意外的可伸缩性

最近,我使用Java Fork-Join运行了一些可扩展性实验。 在这里,我使用了非默认的ForkJoinPool构造函数ForkJoinPool(int parallelism) ,将所需的并行性(#workers)作为构造函数参数传递。

具体来说,使用以下代码:

 public static void main(String[] args) throws InterruptedException { ForkJoinPool pool = new ForkJoinPool(Integer.parseInt(args[0])); pool.invoke(new ParallelLoopTask()); } static class ParallelLoopTask extends RecursiveAction { final int n = 1000; @Override protected void compute() { RecursiveAction[] T = new RecursiveAction[n]; for(int p = 0; p < n; p++){ T[p] = new DummyTask(); T[p].fork(); } for(int p = 0; p = 0; p--){ T[p].join(); } */ } } static public class DummyTask extends RecursiveAction { //performs some dummy work final int N = 10000000; //avoid memory bus contention by restricting access to cache (which is distributed) double val = 1; @Override protected void compute() { for(int j = 0; j < N; j++){ if(val < 11){ val *= 1.1; }else{ val = 1; } } } } 

我在具有4个物理核心和8个逻辑核心的处理器上获得了这些结果(使用java 8:jre1.8.0_45):

T1:11730

T2:2381(加速:4,93)

T4:2463(加速:4,76)

T8:2418(加速:4,85)

在使用java 7(jre1.7.0)时,我得到了

T1:11938

T2:11843(加速:1,01)

T4:5133(加速:2,33)

T8:2607(加速:4,58)

(其中TP是以ms为单位的执行时间,使用并​​行度级别P)

虽然两个结果让我感到惊讶,后者我可以理解(连接将导致1个工作程序(执行循环)阻塞,因为它无法识别它可以在等待时从其本地队列处理其他待处理的虚拟任务)。 然而,前者让我感到困惑。

顺便说一句:在计算已启动但尚未完成的虚拟任务的数量时,我发现在某个时间点,并行性2的池中存在多达24个这样的任务……?

编辑:

我使用JMH(jdk1.8.0_45)(选项-bm avgt -f 1)(= 1 fork,20 + 20 iterations)对上面的应用程序进行基准测试。结果如下

T1:11,664

 11,664 ±(99.9%) 0,044 s/op [Average] (min, avg, max) = (11,597, 11,664, 11,810), stdev = 0,050 CI (99.9%): [11,620, 11,708] (assumes normal distribution) 

T2:4,134(加速:2,82)

 4,134 ±(99.9%) 0,787 s/op [Average] (min, avg, max) = (3,045, 4,134, 5,376), stdev = 0,906 CI (99.9%): [3,348, 4,921] (assumes normal distribution) 

T4:2,972(加速:3,92)

 2,972 ±(99.9%) 0,212 s/op [Average] (min, avg, max) = (2,375, 2,972, 3,200), stdev = 0,245 CI (99.9%): [2,759, 3,184] (assumes normal distribution) 

T8:2,845(加速:4,10)

 2,845 ±(99.9%) 0,306 s/op [Average] (min, avg, max) = (2,277, 2,845, 3,310), stdev = 0,352 CI (99.9%): [2,540, 3,151] (assumes normal distribution) 

乍一看,人们会认为这些可扩展性结果更接近人们预期的结果,即T1 <T2 <T4~T8。 然而,仍然让我感到困惑的是:

  1. java 7和8之间的差异。我想一个解释是执行并行循环的worker在java 8中不会空闲,而是找到其他工作要执行。
  2. 具有2名工人的超线性加速(3x)。 另外,请注意,T2似乎随着每次迭代而增加(见下文,请注意,情况也是如此,尽管P = 4,8的情况较小)。 预热的第一次迭代中的时间与上面提到的相似。 也许预热期应该更长,但是,执行时间增加并不奇怪,即我宁愿期望它减少?
  3. 最后,我仍然发现有一些观察结果表明,与工作线程好奇相比,有更多的启动和未完成的虚拟任务。

>

 Run progress: 0,00% complete, ETA 00:00:40 Fork: 1 of 1 Warmup Iteration 1: 2,365 s/op Warmup Iteration 2: 2,341 s/op Warmup Iteration 3: 2,393 s/op Warmup Iteration 4: 2,323 s/op Warmup Iteration 5: 2,925 s/op Warmup Iteration 6: 3,040 s/op Warmup Iteration 7: 2,304 s/op Warmup Iteration 8: 2,347 s/op Warmup Iteration 9: 2,939 s/op Warmup Iteration 10: 3,083 s/op Warmup Iteration 11: 3,004 s/op Warmup Iteration 12: 2,327 s/op Warmup Iteration 13: 3,083 s/op Warmup Iteration 14: 3,229 s/op Warmup Iteration 15: 3,076 s/op Warmup Iteration 16: 2,325 s/op Warmup Iteration 17: 2,993 s/op Warmup Iteration 18: 3,112 s/op Warmup Iteration 19: 3,074 s/op Warmup Iteration 20: 2,354 s/op Iteration 1: 3,045 s/op Iteration 2: 3,094 s/op Iteration 3: 3,113 s/op Iteration 4: 3,057 s/op Iteration 5: 3,050 s/op Iteration 6: 3,106 s/op Iteration 7: 3,080 s/op Iteration 8: 3,370 s/op Iteration 9: 4,482 s/op Iteration 10: 4,325 s/op Iteration 11: 5,002 s/op Iteration 12: 4,980 s/op Iteration 13: 5,121 s/op Iteration 14: 4,310 s/op Iteration 15: 5,146 s/op Iteration 16: 5,376 s/op Iteration 17: 4,810 s/op Iteration 18: 4,320 s/op Iteration 19: 5,249 s/op Iteration 20: 4,654 s/op 

你的例子中没有任何关于你如何做这个基准的东西。 看起来你只是在跑步的开始和结束时做了一个毫秒的时间。 这不准确。 我建议你看看这个SO答案并重新发布你的时间。 顺便说一句,jmh基准测试将成为Java9的标准,这就是你应该使用的。

编辑:

您承认可伸缩性结果符合您的预期。 但是你说你对结果仍然不满意。 现在是时候查看代码了。

这个框架存在严重问题。 自2010年以来,我一直在写一篇关于它的评论。正如我在这里指出的那样,加入不起作用。 作者尝试了各种方法来解决问题,但问题仍然存在。

将运行时间增加到大约一分钟(n = 100000000)或在compute()中放置一些繁重的计算。 现在在VisualVM或其他分析器中分析应用程序。 这将显示停滞螺纹,过多螺纹等。

如果这对您的问题没有帮助,那么您应该使用调试器查看代码流。 分析/代码分析是您获得满意答案的唯一方法。