Java parallelStream() with reduce() 没有提高性能

Java parallelStream() with reduce() not improving performance

作为对Java 8 新实现的流和自动并行化的测试,我运行进行了以下简单测试:

ArrayList<Integer> nums = new ArrayList<>();
for (int i=1; i<49999999; i++) nums.add(i);

int sum=0;
double begin, end;

begin = System.nanoTime();
for (Integer i : nums) sum += i;
end = System.nanoTime();

System.out.println( "1 core: " + (end-begin) );

begin = System.nanoTime();
sum = nums.parallelStream().reduce(0, Integer::sum);
end = System.nanoTime();

System.out.println( "8 cores: " + (end-begin) );

我认为将一系列整数相加可以充分利用所有 8 个内核,但输出如下所示:

1 core: 1.70552398E8
8 cores: 9.938507635E9

我知道 nanoTime() 在多核系统中存在问题,但我怀疑这就是这里的问题,因为我偏离了一个数量级。

我执行的操作是否简单到 reduce() 所需的开销正在克服多核的优势?

要完全正确比较此,您需要对这两个操作使用相似的开销。

    ArrayList<Integer> nums = new ArrayList<>();
    for (int i = 1; i < 49999999; i++)
        nums.add(i);

    int sum = 0;
    long begin, end;

    begin = System.nanoTime();
    sum = nums.stream().reduce(0, Integer::sum);
    end = System.nanoTime();

    System.out.println("1 core: " + (end - begin));

    begin = System.nanoTime();
    sum = nums.parallelStream().reduce(0, Integer::sum);
    end = System.nanoTime();

    System.out.println("8 cores: " + (end - begin));

这让我着陆

1 core:  769026020
8 cores: 538805164

实际上 parallelStream() 更快。 (注意:我只有 4 个核心,但 parallelSteam() 并不总是使用你所有的核心)

另一件事是装箱和拆箱。 nums.add(i) 需要装箱,进入 Integer::sum 的所有东西需要两个 int 才能拆箱。我将此测试转换为数组以删除它:

    int[] nums = new int[49999999];
    System.err.println("adding numbers");
    for (int i = 1; i < 49999999; i++)
        nums[i - 1] = i;

    int sum = 0;
    System.err.println("begin");
    long begin, end;

    begin = System.nanoTime();
    sum = Arrays.stream(nums).reduce(0, Integer::sum);
    end = System.nanoTime();

    System.out.println("1 core: " + (end - begin));

    begin = System.nanoTime();
    sum = Arrays.stream(nums).parallel().reduce(0, Integer::sum);
    end = System.nanoTime();

    System.out.println("8 cores: " + (end - begin));

这给出了一个意想不到的时机:

1 core:   68050642
8 cores: 154591290

使用常规 int 的线性化简要快得多(1-2 个数量级),但只有并行化简时间的 1/4 左右,而且速度更慢。 我不确定为什么会这样,但这确实很有趣!

做了一些分析,结果是 fork() 并行流的方法非常昂贵,因为使用 ThreadLocalRandom,它调用网络接口作为它的种子!这非常慢,也是 parallelStream()stream()!

慢的唯一原因

我的一些VisualVM数据:(忽略await()时间,这是我使用的方法,所以我可以跟踪程序) 对于第一个例子:https://www.dropbox.com/s/z7qf2es0lxs6fvu/streams1.nps?dl=0 第二个例子:https://www.dropbox.com/s/f3ydl4basv7mln5/streams2.nps?dl=0

TL;DR:在您的 Integer 案例中,它看起来像是并行获胜,但是 int 案例有一些开销使并行变慢。

您的流示例对每个数字都有 2 次拆箱 (Integer.sum(int,int)) 和一次装箱(结果 int 必须转换回 Integer),而 for 循环有只有一个开箱。所以两者没有可比性。

当您计划使用整数进行计算时,最好使用 IntStream:

nums.stream().mapToInt(i -> i).sum();

这会给您带来类似于 for 循环的性能。并行流在我的机器上仍然较慢。

最快的替代方法是这个顺便说一句:

IntStream.rangeClosed(0, 49999999).sum();

速度提高了一个数量级,而且没有先构建列表的开销。当然,它只是这个特殊用例的替代方案。但它表明重新思考现有方法而不是仅仅 "add a stream".

是值得的