【问题标题】:The costs of streams and closures in Java 8Java 8 中流和闭包的成本
【发布时间】:2014-12-31 02:12:17
【问题描述】:

我正在重写一个应用程序,该应用程序涉及使用 Java 8 处理大约 1000 万个对象,我注意到流可以使应用程序减慢 25%。有趣的是,当我的集合也为空时也会发生这种情况,因此它是流的恒定初始化时间。要重现该问题,请考虑以下代码:

    long start = System.nanoTime();
    for (int i = 0; i < 10_000_000; i++) {
        Set<String> set = Collections.emptySet();
        set.stream().forEach(s -> System.out.println(s));
    }
    long end = System.nanoTime();
    System.out.println((end - start)/1000_000);

    start = System.nanoTime();
    for (int i = 0; i < 10_000_000; i++) {
        Set<String> set = Collections.emptySet();
        for (String s : set) {
            System.out.println(s);
        }
    }
    end = System.nanoTime();
    System.out.println((end - start)/1000_000);

结果如下:224 vs. 5 ms。

如果我直接在设置上使用forEach,即set.forEach(),结果将是:12 vs 5ms。

最后,如果我在外面创建一次闭包

Consumer<? super String> consumer = s -> System.out.println(s);

并使用set.forEach(c) 结果将是 7 vs 5 ms。

当然,数字很小,我的基准测试非常原始,但是这个例子是否表明初始化流和闭包有开销?

(实际上,由于set 是空的,在这种情况下,闭包的初始化成本应该不重要,但是,我应该考虑提前创建闭包而不是即时创建闭包)

【问题讨论】:

  • 我也遇到过这个问题,让我非常沮丧和失望。制作一百万个条目,对它们进行随机播放,并尝试使用常规 for 循环和使用 java 8 的 findfirst 找到第一个,并尝试为它们计时声明
  • Clojure != 闭包,Consumer 不是闭包。 en.wikipedia.org/wiki/Closure_%28computer_programming%29
  • 发布与valid performance benchmark 讨论 Lambda 与 Anon 类的帖子
  • 肯定有一些启动开销。但在许多情况下,一旦支付了启动开销,流就会比相应的 for 循环。所以你应该用实际数据做一些测试。 (此外,您的测量方法完全失效,因此您得到的大多是无用的数字。)简短的回答是“是的,有一些”——但如果您想真正了解,请尝试更新您的应用并使用真实的测量其性能数据。
  • 看看 Sergey Kukcenko 在 2013 年 JVM 语言峰会上的演讲(经过严格测量)。几乎所有“行为良好”的流管道(例如 filter-map-reduce),其来源都是一旦您有足够的数据来克服启动差异,Collection 将优于相应的 for 循环,因为元素访问路径要快得多;与迭代器相比,流的 O(n) 项的系数要小。最终占主导地位。

标签: java lambda java-8 java-stream


【解决方案1】:

您在此处看到的成本根本与“闭包”无关,而是与 Stream 初始化的成本有关。

让我们来看看你的三个示例代码:

for (int i = 0; i < 10_000_000; i++) {
    Set<String> set = Collections.emptySet();
    set.stream().forEach(s -> System.out.println(s));
}

这会在每个循环中创建一个新的Stream 实例;至少对于前 10k 次迭代,见下文。在这 10k 次迭代之后,好吧,JIT 可能足够聪明,可以看出它无论如何都是空操作。

for (int i = 0; i < 10_000_000; i++) {
    Set<String> set = Collections.emptySet();
    for (String s : set) {
        System.out.println(s);
    }
}

JIT 再次启动:空集?好吧,这是一个无操作的故事结束。

set.forEach(System.out::println);

为集合创建了一个Iterator,它总是为空的?同样的故事,JIT 开始了。

您的代码一开始的问题是您没有考虑 JIT;对于实际测量,在测量之前至少运行 10k 循环,因为 JIT 需要执行 10k 次(至少,HotSpot 以这种方式运行)。


现在,lambdas:它们是调用站点,它们只链接一次;但当然,初始链接的成本仍然存在,并且在您的循环中,您包括此成本。在进行测量之前尝试只运行一个一个循环,这样就可以避免这种成本。

总而言之,这不是一个有效的微基准测试。使用 caliper 或 jmh,真正衡量性能。

了解 lambda 是如何工作的精彩视频 here。现在有点老了,而且 JVM 比当时使用 lambdas 好得多。

如果您想了解更多信息,请查找有关 invokedynamic 的文献。

【讨论】:

  • 很好的参考invokedynamic和JIT。您还有其他关于优化新手的优秀 JIT 参考文档吗?
  • 谢谢!我应该阅读它并观看视频,但在此之前有一个简单的问题:当你说:“但初始链接的成本仍然存在时,我只是没有得到一点,当然,在你的循环中,你包括这笔费用。” Java 编译器不会在运行之前翻译 lambda,还是在运行时发生某些事情?这是 invokedynamic 的用途吗?
  • @Ali “Java 编译器不是在运行前翻译 lambda,还是在运行时发生了什么?这就是 invokedynamic 的用途吗?”是的,这就是 invokedynamic 的用途。生成的调用站点有一个所谓的引导方法,invokedynamic 向这个引导方法询问它应该链接什么代码。理论上,如果参数的类型或什至 number 发生变化,则 可能 会再次查询引导方法,但在 Java 中绝不会出现这种情况。这在 JVM 上实现的动态语言(例如 Jython 和 JRuby)中的情况。
  • @Ali 并且一旦代码被链接,JIT 可以优化它到它的核心内容,这是另一个很棒的功能;这就是为什么,例如,JRuby 的性能优于原生 Ruby 实现——到目前为止!
  • @EddieB this video 可能感兴趣;至于它是如何在内部工作的,我不知道;)它需要 CS 专家才能理解。但是这个视频以非常容易理解的方式解释了内联、锁粗化、循环展开、逃逸分析等。
猜你喜欢
  • 2017-07-12
  • 2016-12-06
  • 1970-01-01
  • 2011-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多