【问题标题】:Why is parallel stream slower?为什么并行流更慢?
【发布时间】:2016-12-29 13:02:56
【问题描述】:

我正在玩无限流,并制作了这个程序用于基准测试。基本上,您提供的数字越大,完成的速度就越快。然而,我惊讶地发现,与顺序流相比,使用并行流导致性能呈指数级下降。直觉上,人们会期望在多线程环境中更快地生成和评估无限的随机数流,但情况似乎并非如此。这是为什么呢?

    final int target = Integer.parseInt(args[0]);
    if (target <= 0) {
        System.err.println("Target must be between 1 and 2147483647");
        return;
    }

    final long startTime, endTime;
    startTime = System.currentTimeMillis();

    System.out.println(
        IntStream.generate(() -> new Double(Math.random()*2147483647).intValue())
        //.parallel()
        .filter(i -> i <= target)
        .findFirst()
        .getAsInt()
    );

    endTime = System.currentTimeMillis();
    System.out.println("Execution time: "+(endTime-startTime)+" ms");

【问题讨论】:

  • 并行化小任务总是会变慢。多线程有足够的开销,任务需要证明成本是合理的,否则您将看不到任何收益。此外,1 次测试毫无意义。至少,将其循环并取平均值。
  • @Carcigenicate 除此之外,我相信 Math.random() 会减慢速度 :-) 如果有兴趣,请查看我的答案。
  • 你知道有一个内置的Random.ints() 流来获取随机数流吗?
  • 您的示例遇到了并行化不佳的几个风险因素。首先,findFirst() 与流的遇到顺序相关联,这阻碍了库的并行化能力(您应该改用findAny()。)其次,您只是没有为每个元素做太多工作,这意味着创建、调度和协调任务的开销可能会超过实际工作。每个元素的工作也没有表现出太多的局部性。所以并行加速的条件并不真正存在。 (此外,您的基准方法可能会给出毫无意义的数字。)
  • @Holger 是的,generate() 的结果是无序的。但是并行性和遇到顺序之间的关系是微妙的,并且经常是不能很好地并行化的原因。所以我觉得值得提出这种联系,即使它不适用于这个特定代码sn-p。

标签: java java-8 java-stream


【解决方案1】:

我完全同意其他 cmets 和答案,但如果目标非常低,您的测试确实表现得很奇怪。在我的普通笔记本电脑上,当给定非常低的目标时,并行版本平均慢约 60 倍。这种极端差异无法用流 API 中的并行化开销来解释,所以我也很惊讶 :-)。 IMO 罪魁祸首在这里:

Math.random()

在内部,此调用依赖于java.util.Random 的全局实例。在documentation of Random 中写道:

java.util.Random 的实例是线程安全的。然而,并发 跨线程使用相同的 java.util.Random 实例可能会遇到 争用和随之而来的糟糕表现。考虑改为使用 多线程设计中的 ThreadLocalRandom。

所以我认为与顺序执行相比,并行执行的性能确实很差,这是由随机的线程争用而不是任何其他开销来解释的。如果您改用ThreadLocalRandom(如文档中所建议的那样),则性能差异不会那么显着。另一种选择是实施更高级的号码供应商。

【讨论】:

  • 查看Math.random() 的内部实现——它是用原子完成的,没有任何锁。很难相信它会带来这样的差异
  • 是的,我刚刚检查过了。但与线程局部随机相比,它确实引入了巨大的差异,正如文档所述......:O
  • @SergeyFedorov,虽然原子没有锁,但它们会大大降低原子操作的 do-while 循环的性能,直到状态被认为是一致的。问题是 Unsafe.compareAndSwap() - 他们在内部使用繁忙的等待。在高竞争的情况下,成本会成倍增加,并且在一组操作上使用同步块会更好。只有测试才能说出最佳策略。当然,ThreadLocalRandom 每次都会胜过这两种方法。
  • 您确定CAS操作使用等待吗?有什么证据吗?
  • @Sergey Fedorov:CAS 不使用等待,但 CAS 可能会失败,在这种情况下,调用代码必须重试操作(除非可以选择失败)。重复重试就像轮询,这就是 Alex Pakka 所说的“忙等待”。这不是将线程置于等待状态的意义上的等待。
【解决方案2】:

根据各种答案的建议,我想我已经解决了。我不确定确切的瓶颈是什么,但在 i5-4590T 上,具有以下代码的并行版本比顺序变体执行得更快。为简洁起见,我只包含了(重构的)代码的相关部分:

static IntStream getComputation() {
    return IntStream
            .generate(() -> ThreadLocalRandom.current().nextInt(2147483647));
}

static void computeSequential(int target) {
    for (int loop = 0; loop < target; loop++) {
        final int result = getComputation()
                    .filter(i -> i <= target)
                    .findAny()
                    .getAsInt();
        System.out.println(result);
    }
}

static void computeParallel(int target) {
     IntStream.range(0, target)
                .parallel()
                .forEach(loop -> {
                    final int result = getComputation()
                        .parallel()
                        .filter(i -> i <= target)
                        .findAny()
                        .getAsInt();
                    System.out.println(result);
                });
}

编辑:我还应该注意,我把它全部放在一个循环中以获得更长的运行时间。

【讨论】:

  • 这不是一个有效的测试,因为您现在正在另一个并行操作中执行并行流操作,这将内部操作(您问题的实际操作)基本上变成了顺序操作,因为工作以完全不同的方式进行拆分,即范围 IntStream.range(0, target) 将被并行处理,而不是随机数序列。
  • @Holger 但是在多台机器上进行测试后,我仍然得到了我期望的结果。在四核 CPU 上,对于 1000 以下的大多数数字,并行版本的执行速度比顺序版本快 3 倍以上。
  • 实际上我尝试将 .parallel() 添加到顺序版本中,但它仍然较慢,所以原来加速来自 InstStream.range 而不是随机数流。
【解决方案3】:

第一次将工作传递给多个线程的成本很高。这个成本是相当固定的,所以即使你的任务是微不足道的,开销也相对较高。

您遇到的一个问题是,效率极低的代码是确定解决方案执行情况的一种非常糟糕的方法。此外,它第一次运行的方式和几秒钟后的运行方式通常会有 100 倍的差异(可能更多),我建议使用一个已经最优的示例,然后才尝试使用多个线程。

例如

long start = System.nanoTime();
int value = (int) (Math.random() * (target+1L));
long time = System.nanoTime() - value;
// don't time IO as it is sooo much slower
System.out.println(value);

注意:在代码预热和编译之前,这不会有效。即忽略此代码运行的前 2-5 秒。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-28
    • 1970-01-01
    • 1970-01-01
    • 2010-09-22
    相关资源
    最近更新 更多