【问题标题】:Java 8, first processing of a list is slower than the subsequent processingJava 8,列表的第一个处理比后续处理慢
【发布时间】:2016-03-02 21:53:27
【问题描述】:

我正在运行一些测试(非常基础,没什么花哨的),以检查 Java 8 流和 lambda 的性能。使用 1000 万个 POJOS 的 ArrayList,我要做的就是获取 BigDecimal 字段的平均值。为了采集多个样本,我运行了五次该过程,令我惊讶的是,这五次运行中的第一次比其他运行慢得多。我第一次得到的值是 0.38 秒,其他四个是 0.04 秒。这快了 10 倍!!!我还使用老派for(Pojo p : pojos) 进行了相同的测试,结果相似。为什么会发生这种情况,我该如何利用它?我使用的代码是:

for (int i = 0; i < 5; i++) {
    long init = System.nanoTime();
    BigDecimal sum = lista.parallelStream().map(x -> x.getCosto()).reduce(BigDecimal.ZERO, BigDecimal::add);
    BigDecimal avg = sum.divide(BigDecimal.valueOf(registros));
    long end = System.nanoTime();
    System.out.println("End of processing: " + avg + " in "
            + ((end - init) / 1000000000.0) + " seconds.");
}

【问题讨论】:

  • 不幸的是,进行性能测试并不是那么容易,尤其是在结合 lambda 表达式和方法引用时。您需要为此使用适当的工具,例如 JMH 框架。
  • 我不同意重复标记。 OP 询问为什么第一个处理速度较慢。确实如此。即使 OP 使用 JMH 重写基准,第一次迭代也会比后续迭代慢得多。考虑到我们所说的是几十毫秒,而不是微秒或纳秒,OPs 测量在方法上并不是那么糟糕。

标签: performance java-8 java-stream


【解决方案1】:

当您第一次调用 Stream API 时,它需要一个持续的延迟,包括以下步骤:

  • java.util.stream 包中加载许多帮助类
  • java.lang.invoke 包(如 LambdaMetafactory)加载 lambda 生成类。
  • 为流管道中涉及的 lambda 和方法引用(包括 Stream API 内部使用的 lambda)生成运行时表示。
  • 所有这些字节码的分层编译(解释器 -> C1 JIT -> C2 JIT)。 C2 JIT 编译(生成最快的代码)仅在特定数量的方法调用(如 5000)或特定数量的后边之后触发(如果方法内部有大循环,则循环迭代;如 40000)。当大多数代码不是 C2 编译的时,它的工作速度要慢得多。此外,JIT 编译器线程需要一些 CPU 时间,这些时间可以用于实际计算。
  • 对于并行流:初始化公共ForkJoinPool,创建新线程。

所有这些步骤只执行一次。当您再次使用 Stream API 时,大部分工作已经完成,因此连续启动要快得多。

在您的特定情况下,您正在大量使用堆,因此堆扩大也可能是额外缓慢的原因。如果你的-Xms 默认值太小,那么垃圾收集器会执行几个完整的 gc 循环,直到它将堆扩大到合适的大小。您可以使用 Xms==Xmx(例如 -Xmx1G -Xms1G)运行测试,这可能会提高第一次迭代速度。

【讨论】:

  • 也许值得将一般答案添加到“我如何利用它?”问题的一部分:通常,避免代码重复,创建可重用的类等。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-14
  • 2015-01-12
  • 2013-09-24
  • 1970-01-01
  • 2015-03-22
  • 2021-04-25
  • 1970-01-01
相关资源
最近更新 更多