【问题标题】:Java: Why parallel stream works slow at the start of application?Java:为什么并行流在应用程序启动时运行缓慢?
【发布时间】:2020-10-28 23:26:38
【问题描述】:

我决定在 Java 1.8.045,CPU:AMD a10-6800k 4-Cores x64 上测量常规流和并行流之间的性能差异:
如果我将并行流放在顺序流之前,它的工作速度比顺序流慢。
我怀疑基础代码中的第一个流无论如何都会运行缓慢。
我认为问题在于 JVM 启动需要时间。我什至将 Thread.sleep(100000) 放在基本代码的开头。它没有帮助。
如果我放置第一个顺序流,第二个是并行流 - 一切看起来都正确,但是 ... :)

计划测试并行流与常规流:加载 6M 文本文件,计算大小超过 12 个符号的单词数量。
我通过两个分开的流来完成它,并测量每个流的性能。 排除任何不必要的影响:

  1. 我将文件两次加载到两个单独的 ArrayLists(特定流的每个数组列表)。
  2. 我通过旧的 Java 方法加载数据以排除来自这方面的任何影响。
  3. 我什至将字符串放入字符串池中。
  4. 我提出了 -Xms 和 -Xmx :VM 选项:
    -verbose:gc -Xms1000000k -Xmx3000000k
    我不明白:[GC(分配失败)65536K->9238K(251392K),0.0085762 秒]

如果我先放并行流,我得到:
并行流持续时间:287.271699
并行流的字数:7588
常规流时长:138.9853
按常规流计算的字数:7588

并行流时长几乎是常规流时长的 2 倍。

如果我先放常规流,我会得到:
常规流持续时间:336.4724
按常规流计算的字数:7588
并行流时长:98.5675
并行流的字数:7588

我知道 JVM 可能会启动它应该很热,我应该测量几次。 是的,如果我将第一个 Stream 放入循环并在同一过程中进行多次计算,第一次计算很慢,第二次计算更快。 我想在 JVM 启动时期望并行流的稳定行为也是正常的。 GC 对应用程序启动时的性能是否有其他影响? 为什么会发生?我们使用不同的集合。为什么基础代码中的第一个流计算总是很慢?

我的基本代码:

import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.regex.Pattern;

public class StreamsExperiment {
    public static void main(String[] args) throws InterruptedException {
        try(BufferedReader bufferedReaderParallel = new BufferedReader(new FileReader("resource//war_and_peace.txt"))){
            String line = bufferedReaderParallel.readLine();
            ArrayList<String> lineListParallel = new ArrayList<>();
            while (line != null) {
                line.intern();
                lineListParallel.add(line);
                line = bufferedReaderParallel.readLine();
            }
            long startTimeParallel =  System.nanoTime();
            long bigWordsCount2 = lineListParallel.stream().parallel().flatMap(x -> Pattern.compile("\\PL+").splitAsStream(x)).filter(w -> w.length() > 12).count();
            long stopTimeParallel = System.nanoTime();
            System.out.println("Parallel stream duration: " + (stopTimeParallel - startTimeParallel) / 1000000.0);
            System.out.println("Words count by parallel stream : " + bigWordsCount2);
        } catch (IOException e){
            System.out.println("I/O error : " + e);
        }

        try(BufferedReader bufferedReaderRegular = new BufferedReader(new FileReader("resource//war_and_peace.txt"))){
            String line = bufferedReaderRegular.readLine();
            ArrayList<String> lineListRegular = new ArrayList<>();
            while (line != null) {
                line.intern();
                lineListRegular.add(line);
                line = bufferedReaderRegular.readLine();
            }
            long startTimeRegular =  System.nanoTime();
            long bigWordsCount1 = lineListRegular.stream().flatMap(x -> Pattern.compile("\\PL+").splitAsStream(x)).filter(w -> w.length() > 12).count();
            long stopTimeRegular = System.nanoTime();
            System.out.println("Regular stream duration: " + (stopTimeRegular - startTimeRegular) / 1000000.0);
            System.out.println("Words count by regular stream : " + bigWordsCount1);
        } catch (IOException e){
            System.out.println("I/O error : " + e);
        }
    }
}

【问题讨论】:

  • 与问题无关,但不应该将Pattern.compile("\\PL+") 定义为变量吗?为每条记录编译它也可能会对性能产生一些影响。
  • 当您尝试多次运行这些代码块 (10+) 时会发生什么?并尝试交替运行它们。
  • 这不是衡量性能的方式。请看Microbenchmarking with JMH
  • 为了证明你的“基准”是多么荒谬,只需交换两个测试,先做顺序,然后再做并行。除此之外,我建议了解Files.readAllLines(…) ant,好吧,既然阅读不是你的基准测试的一部分,你为什么要这样做两次而不是重复使用列表?而line.intern(); 调用毫无意义,它白白浪费了 CPU 周期。
  • 既然你已经注意到自己总是第一个流操作比第二个慢,不清楚你为什么还在问parallel 流。在您的 main 方法之前,甚至没有加载 Stream API 类,更不用说初始化、编译和/或优化了。也有可能之前没有使用过正则表达式类。因此,您正在测量加载、初始化和编译/优化大量类所需的时间。

标签: java performance parallel-processing java-stream startup


【解决方案1】:

衡量绩效

用 Java 衡量性能很棘手。

Java 使用所谓的“热点”即时编译器。它首先以非优化的形式启动程序,统计监控其执行情况,找到消耗CPU的“热点”并对其进行优化(当然,真正的过程比这种简化的解释要复杂得多……)。

因此,任何给定的代码首先会运行缓慢,并且只有在重复相同的代码模式之后才会变得更快。 JVM 不会在启动期间优化您的代码,只会在执行时优化。这就是为什么你的Thread.sleep() 没有帮助的原因,优化只能通过重复运行一段代码来触发。

现在你有了三个主要的代码部分:

  • 常规流框架
  • 并行流框架
  • 两个版本共享的“流体”

这三个部分中的每一个都需要一些预热迭代,直到获得最终性能。

如果您从常规流开始,那么这将受到未优化代码以及优化常规流和正文部分的负担的影响。常规流的后续重复已经获得了完全优化的环境并全速运行。

如果您随后运行并行流,则只有并行流代码仍需要优化,但它将受益于来自常规流执行的优化体。

经验法则:

只有在一段足够长的预热阶段反复执行后,才能衡量任何给定代码的性能。

提高性能

使用分析器而不是猜测。

不要猜测你的程序在哪里花费时间,找一个测量工具(称为“分析器”),让它告诉你在哪里消耗 CPU 时间。从 30 多年的软件开发经验来看,我可以告诉你,猜测几乎找不到真正的瓶颈。

您对性能瓶颈进行了一些猜测,结果证明并没有真正解决问题。

正如@Amongalen 所建议的那样,它很可能是流体内的Pattern.compile(),但这也是猜测。它也可能只是文件的BufferedReader,或者只是等待I/O。在测量之前你无法知道它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多