并行处理——意外的可伸缩性导致了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 < n; p++){
            T[p].join();
        }
        /*
        //The problem does not occur when tasks are joined in the reverse order, i.e.
        for(int p = n-1; 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 (speedup: 4,93)

T4: 2463 (speedup: 4,76)

T8: 2418 (speedup: 4,85)

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

T1: 11938

T2: 11843 (speedup: 1,01)

T4: 5133 (speedup: 2,33)

T8: 2607 (speedup: 4,58)

(其中TP是以毫秒为单位的执行时间,使用并行级别p)

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

BTW:当计算启动但尚未完成的虚拟任务的数量时,我发现在某个时间点并行度为2的池中存在多达24个这样的任务…?

编辑:

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

T1: 11664

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 (speedup: 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 (speedup: 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 (speedup: 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

  1. java 7和java 8之间T2的差异。我想有一种解释在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年以来,我一直在写一篇关于它的评论。正如我在这里指出的,join不起作用。作者已经尝试了各种方法来解决这个问题,但问题仍然存在。

将运行时间增加到大约1分钟,(n=100000000)或者在compute()中进行一些繁重的计算。现在在VisualVM或其他分析器中分析应用程序。这将向您显示正在运行的线程、过多的线程等。

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

最新更新