【问题标题】:Puzzled with Java8 Stream performance对 Java8 Stream 性能感到困惑
【发布时间】:2015-01-22 11:12:02
【问题描述】:

我不明白为什么要使用 Stream api 在同一个数组上进行多次迭代 导致这样的表现!

请看下面的代码。

public class WhyIsDifferent {

public static void main(String[] args) {

    int[] values = getArray();
    Iterate(values, 598, 600); // 70 ms
    Iterate(values, 200, 202); // 0 ms
    Iterate(values, 700, 702); // 0 ms
    Iterate(values, 300, 310); // 1 ms
}

public static void Iterate(int[] values, int from, int to) {
    long start = System.currentTimeMillis();
    IntStream.of(values).filter(i -> i < to && i > from)
    .forEach(i -> 
        System.out.println(i) // do a something
    );
    System.out.println("Time:" + (System.currentTimeMillis() - start));
}

public static int[] getArray() {
    int[] values = new int[1000];
    for (int i = 0; i < 1000; i++) {
        values[i] = i;
    }
    return values;
}
}

肯定 JVM 优化了代码,但我不知道这是怎么发生的??太棒了! 你知道为什么会这样吗?

--

我正在 Ubuntu 14.04//Oracle jdk/intel cpu 上进行测试。

【问题讨论】:

  • 这是JIT 的工作,我的朋友。
  • @LuiggiMendoza 说了什么;更重要的是,您达到了“最佳状态”,因为默认情况下,Oracle JVM 至少会在同一代码块执行 1000 次后进行优化。
  • 对它的一种解释是 lambda 只不过是一个“调用站点”;在您的情况下,鉴于您的 lambda 的简单性,JVM 肯定会完全内联它。在 YouTube 上搜索“lambda peek under the hood”并观看视频:它非常有启发性。事实上,第一次迭代的大部分成本可能是由于最初的链接!
  • 在您的情况下,这非常简单:整个运行时中只有一个现有的 Predicate 实现。在更复杂的用例中,内置的自我分析器将检测实际执行的代码路径(并且值得优化)。
  • 这与 lambdas 无关。 Iterate 的第一次调用总是比较慢;这就是虚拟机的工作方式(解释一段时间,然后进行一些编译等)您需要更仔细地测量。尝试使用 JMH 之类的东西。

标签: java performance lambda java-8 performance-testing


【解决方案1】:

它不是 JIT 编译器。这 70 毫秒中的大部分时间都花在了整个 lambda 子系统的初始化上(该逻辑的入口点可能是 LambdaMetaFactory 类),并且还花了不少时间在 lambda 引导调用(链接 em> 阶段,如用户 fge 所述)。看看这个方法,和你的一样,但所有的步骤都是分开测量的(我使用nanoTime):

public static void Iterate(int[] values, int from, int to) {
  long start = System.nanoTime();
  final IntPredicate predicate = i -> i < to && i > from;
  System.out.println("Predicate lambda creation time:" + NANOSECONDS.toMillis(System.nanoTime() - start));
  start = System.nanoTime();
  final IntConsumer action = System.out::println;
  System.out.println("Action lambda creation time:" + NANOSECONDS.toMillis(System.nanoTime() - start));
  start = System.nanoTime();
  final IntStream stream = IntStream.of(values).filter(predicate);
  System.out.println("Stream creation time:" + NANOSECONDS.toMillis(System.nanoTime() - start));
  start = System.nanoTime();
  stream.forEach(action);
  System.out.println("Stream consumption time:" + NANOSECONDS.toMillis(System.nanoTime() - start));
}

这是我机器上打印的内容:

Predicate lambda creation time:53
Action lambda creation time:2
Stream creation time:2
599
Stream consumption time:1
Predicate lambda creation time:0
Action lambda creation time:0
Stream creation time:0
201
... all timings zero from here on...

您可以看到,第一次调用的全部开销都在 lambda 创建部分(仅在第一次运行时,包括一般初始化和链接),并且流创建也需要一些时间。在所有情况下,实际的流消耗都需要零时间。

对于当前版本的 HotSpot,您绝对需要牢记这种效果:lambda bootstrap 是一件昂贵的事情。

最后说明:如果您重新排序 lambda 创建语句,您会发现大部分时间都停留在要创建的 第一个 lambda 上。这向我们表明,它实际上只是承担大部分初始化成本的 lambda 的第一次整体创建。

【讨论】:

  • 查看我的新编辑,我已经拆分了所有创建步骤(两个 lambda 表达式和流)。现在流消费(forEach 调用)即使在第一次运行时也不需要时间。数组本身在这里无关紧要 --- 除非你把它 真的 很大。
  • 关于阅读材料,我推荐LambdaMetaFactory的javadoc作为一个很好的起点。
  • @fge “足够快”,如果有的话,是相对的 :) 当我说“昂贵”时,我的意思是它很容易使执行流逻辑的实际时间相形见绌。顺便说一句,我对您对 Scala 的评价感到惊讶,因为它是一种静态类型的语言。特定调用站点的参数类型如何更改?你的意思是不同的子类型可以在调用中涉及?但它与 Java 没有什么不同。
  • 实际上,你的数字反驳了你的说法。如果 lambda 引导程序很昂贵,则操作 lambda 创建和谓词 lambda 创建时间花费相同的时间。然而,第一个花费的时间要长得多,因为它是普通的类加载(LambdaMetaFactory 和它是必需的类)是昂贵的,而不是引导本身。即使是第二个也需要加载IntPredicate。顺便说一句,问题不是“为什么第一次执行速度慢”,而是“为什么后面的执行速度这么快”。在这方面,你不能忽视 JIT/HotSpot 优化的影响。
  • @Holger 看看我的“最后说明”,它涵盖了您的第一次投诉。至于“为什么后续执行这么快”,根源在于 OP 的错误思维:他绝对应该考虑在 1000 个简单操作上花费 70 ms 非常慢,而不是花费不到 1 ms 异常快。此外,我准确解释了 JIT 无关紧要的原因:当数组大小设置为零时,观察到的时间没有差异。不过,您在一点上是对的:我应该从 lambda 引导程序中额外拆分类初始化时间。
猜你喜欢
  • 2017-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-07
  • 2013-02-25
相关资源
最近更新 更多