【问题标题】:Java parallel stream performanceJava 并行流性能
【发布时间】:2026-01-10 08:40:01
【问题描述】:

在玩弄新的 Java 流时,我注意到一些与并行流的性能相关的奇怪现象。我使用了一个简单的程序,它从文本文件中读取单词并计算长度 > 5 的单词(测试文件有 30000 个单词):

    String contents = new String(Files.readAllBytes(Paths.get("text.txt")));
    List<String> words = Arrays.asList(contents.split("[\\P{L}]+"));
    long startTime;
    for (int i = 0; i < 100; i++) {
        startTime = System.nanoTime();
        words.parallelStream().filter(w -> w.length() > 5).count();
        System.out.println("Time elapsed [PAR]: " + (System.nanoTime() - startTime));
        startTime = System.nanoTime();
        words.stream().filter(w -> w.length() > 5).count();
        System.out.println("Time elapsed [SEQ]: " + (System.nanoTime() - startTime));
        System.out.println("------------------");
    }

这会在我的机器上生成以下输出(我只提到第一次和最后 5 次循环迭代):

Time elapsed [PAR]: 114185196
Time elapsed [SEQ]: 3222664
------------------
Time elapsed [PAR]: 569611
Time elapsed [SEQ]: 797113
------------------
Time elapsed [PAR]: 678231
Time elapsed [SEQ]: 414807
------------------
Time elapsed [PAR]: 755633
Time elapsed [SEQ]: 679085
------------------
Time elapsed [PAR]: 755633
Time elapsed [SEQ]: 393425
------------------
...
Time elapsed [PAR]: 90232
Time elapsed [SEQ]: 163785
------------------
Time elapsed [PAR]: 80396
Time elapsed [SEQ]: 154805
------------------
Time elapsed [PAR]: 83817
Time elapsed [SEQ]: 154377
------------------
Time elapsed [PAR]: 81679
Time elapsed [SEQ]: 186449
------------------
Time elapsed [PAR]: 68849
Time elapsed [SEQ]: 154804
------------------

为什么第一个处理比其他处理慢 100 倍?为什么并行流在第一次迭代中比顺序流慢,但在最后一次迭代中快两倍?为什么顺序流和并行流都会随着时间的推移变得更快?这和循环优化有关吗?

后期编辑:根据 Luigi 的建议,我使用 JUnitBenchmarks 实现了基准测试:

List<String> words = null;

@Before
public void setup() {
    try {
        String contents = new String(Files.readAllBytes(Paths.get("text.txt")));
        words = Arrays.asList(contents.split("[\\P{L}]+"));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@BenchmarkOptions(benchmarkRounds = 100)
@Test
public void parallelTest() {
    words.parallelStream().filter(w -> w.length() > 5).count();
}

@BenchmarkOptions(benchmarkRounds = 100)
@Test
public void sequentialTest() {
    words.stream().filter(w -> w.length() > 5).count();
}

我还将测试文件的字数提高到 300000。新结果是:

Benchmark.sequentialTest:[测量 105 轮中的 100 轮,线程数:1 (顺序)]

round:0.08 [+- 0.04],round.block:0.00 [+- 0.00],round.gc:0.00 [+- 0.00], GC.calls: 62, GC.time: 1.53, time.total: 8.65, time.warmup: 0.81, time.bench: 7.85

Benchmark.parallelTest:[测量 105 轮中的 100 轮,线程数:1 (顺序)]

round:0.06 [+- 0.02],round.block:0.00 [+- 0.00],round.gc:0.00 [+- 0.00], GC.calls: 32, GC.time: 0.79, time.total: 6.82, time.warmup: 0.39, time.bench: 6.43

所以初步结果似乎是微基准配置错误造成的……

【问题讨论】:

  • 感谢 Luiggi 提供的真正有用的链接。这解释了第一次处理的缓慢。实施热身后,第一次迭代的性能差异与下一次迭代的性能差异相似。
  • 注意,如果你颠倒你的测试顺序,即先执行顺序然后并行,你会得到完全不同的结果。最好使用像JUnitBenchmark 这样的基准测试框架
  • 添加了 JUnitBenchmark 结果。谢谢你的建议。问题很可能是由我错误的微基准测试代码引起的。

标签: java performance parallel-processing microbenchmark


【解决方案1】:

Hotspot JVM 以解释模式开始执行程序,并在经过一些分析后将常用部分编译为本机代码。因此,循环的初始迭代通常很慢。

【讨论】:

  • 感谢您解释为什么迭代变得更快,Joni。有趣的是,第一次迭代总是比第二次迭代慢 5-6 倍,而下一次迭代的性能大致相似。如果编译为本机代码是在一些迭代之后发生的,为什么第一个会慢得多?此外,为什么并行流最初慢 2 倍但最终变得比顺序流快 2 倍的问题仍然存在。
  • 在一个方法被调用大约 10000 次后触发编译,因此在循环的第一次迭代期间,JVM 会检测到您使用的库代码中的热点并对其进行编译,从而使后续运行更快.您可以启用诊断标志,以便 JVM 打印出它编译的内容和时间。解释并行流的行为需要一些调查..
  • 好的,这解释了我在设置 -XX:+PrintCompilation 标志时得到的输出。但我真的很想知道为什么并行流更慢......我认为,总的来说,我们应该期望并行流更高效。