【问题标题】:Java parallelStream() with reduce() not improving performance带有 reduce() 的 Java parallelStream() 没有提高性能
【发布时间】:2015-01-23 04:47:03
【问题描述】:

作为对 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() 所需的开销超过了多核的优势?

【问题讨论】:

  • 我猜它与自动装箱有关...用 int 数组和计数器测试它?
  • 请注意,parallelSteam() 并不总是使用您的所有内核。
  • 什么自动装箱?一切都是整数。将sum 更改为整数也不会以任何方式影响性能。
  • 另请注意,这不是正确的基准。使用微基准测试工具(如 jmh)真正检查性能差异。
  • 撇开基准测试缺陷不谈,并行流的开销很可能会淹没多核的加速。有关这些问题的讨论,请参阅 When to use Parallel Streams

标签: java optimization concurrency java-8


【解决方案1】:

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

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

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

这将为您提供类似于 for 循环的性能。我的机器上的并行流仍然较慢。

最快的选择是这个顺便说一句:

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

速度提高了一个数量级,并且没有先构建列表的开销。当然,这只是这个特殊用例的替代方案。但它表明,重新考虑现有方法而不是仅仅“添加流”是值得的。

【讨论】:

  • return nums.parallelStream().mapToInt(Integer::intValue).sum(); 运行速度比 for 循环快,但在我的 12 核机器上只有 2 倍。没什么好写的:-(
  • @assylias:你的方法在我的机器上运行得最快
【解决方案2】:

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

    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 的所有内容进行拆箱,这需要两个ints。我将此测试转换为一个数组以删除它:

    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

使用常规整数的线性归约要快得多(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 情况下存在一些开销,这会使并行速度变慢。

【讨论】:

  • 更新了 parallelStream() 的数据缓慢。
  • 您的基准测试有缺陷(不等待 JIT 编译,不使用可能被 JIT 完全跳过的被测操作的结果,...)。我建议使用适当的基准测试环境,例如 jmh。
  • 我知道基准测试存在缺陷,但是我尝试使用的任何基准测试工具最终都会让我失望。如果你有更好的数据,把它放在某个地方,我可以添加它。
  • ,不要使用double 变量来存储long 值。真的很痛……
  • @ıɯɐƃoʇǝızuǝʞ 你的方法完全失败了,所以结果毫无意义。它受到各种启动异常的严重偏见,更糟糕的是,差异如此。如果您有已知的虚假数据,最好避免将其用作性能建议的基础......
猜你喜欢
  • 2022-01-24
  • 1970-01-01
  • 2015-08-09
  • 2016-12-20
  • 2018-10-09
  • 1970-01-01
  • 2021-09-06
  • 2014-06-09
  • 1970-01-01
相关资源
最近更新 更多