【问题标题】:Why does IntStream.range(0, 100000).parallel().foreach take longer then normal for loop为什么 IntStream.range(0, 100000).parallel().foreach 需要比正常循环更长的时间
【发布时间】:2014-11-10 06:48:35
【问题描述】:

我刚开始学习 Java 中的 Streams 和 parallel,我想知道为什么普通的 for 循环比 IntStreamparalleled 在向数组中添加项时花费的时间更少。

package parallel;

import java.util.stream.IntStream;

public class Parallel {

    public static void main(String[] args) {
         final int[] intArray = new int[100000];
        long startTime = System.currentTimeMillis(); 
        IntStream.range(0, 100000).parallel().forEach(i ->  intArray[i]=i);
        long endTime = System.currentTimeMillis();
        System.out.println("Parallel time: " + (endTime-startTime));
        final int[] intArray2 = new int[100000];
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        startTime = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            intArray2[i] = i;
        }
        endTime = System.currentTimeMillis();
        System.out.println("Non parallel time: " + (endTime-startTime));
    }
}

得到这样的结果。

并行时间:110

非并行时间:7

【问题讨论】:

  • 首先,众所周知,基准测试很难准确执行。微基准测试更糟糕,然后就没用了。我会指出,您的数组已经构建完毕,您可以循环并设置一个值,而并行 foreach 没有以预初始化值开始。
  • 有两个不同的数组被填充(intArrayintArray2),所以这两种情况都从一个“干净”的数组开始。
  • 问题在于你的测量方法,句号。这种测量方法的最大问题是它打印一个数字,人们很容易将其解释为有意义。它没有。

标签: java benchmarking java-stream


【解决方案1】:

您对每个元素执行的操作非常简单,只是一个赋值,非常快。在并行版本中,通过启动处理操作的多个线程会产生很多开销。仅此一项可能已经比非并行应用时非常简单的操作花费更长的时间。

此外,在非并行版本中,值以非常线性的方式写入数组,其中 CPU 架构已经有很长一段时间的单威胁/单核优化,编译器和中间编译器也是如此(将代码转换为C 组装)。但是在并行版本中,您可能会遇到冲突,因为每个线程都尝试写入同一个数组(尽管在不同的位置,但可能仍然在同一缓存行上),并且由于多个线程访问数组的不同部分,您也可能会导致缓存未命中,从而减慢速度。

对于更昂贵的操作,并行版本的开销与总成本相比变得更小,最终将导致比非并行情况更快的执行。

【讨论】:

  • 我怀疑它会造成如此大的差异 - 一个有缺陷的基准可能也是解释的一部分。
  • 当然,它应该在适当的 JVM 预热后执行很多次,然后取平均值,否则结果中会有太多的随机噪音。但即使设置正确,我的猜测是使用上面的代码,并行代码仍然会更慢(仅根据个人经验,正确的基准测试将证明我是对还是错;))。
  • 您建议 parallel().forEach 将工作分配给交叉 i 值的线程,例如4 个线程中的每一个都从不同的起点执行i+=4。这将使接口对使用数组进行任何操作都无法使用;当然,一个理智的实现会将范围分解为i 值的连续子范围,因此每个线程只触及一些缓存行。线程启动开销和有缺陷的基准测试(JVM 没有预热、CPU 频率或数组页面错误)仍然足以解释仅超过 400kB 的简单 memset 循环的巨大性能差异。
猜你喜欢
  • 1970-01-01
  • 2011-12-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-06
  • 2022-01-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多