我正在重写一个涉及使用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。
当然,nubmers很小,我的基准测试非常原始,但这个例子是否表明初始化流和闭包有一些开销?
(实际上,由于set
为空,因此闭包的初始化成本在这种情况下不应该很重要,但是,如果我考虑先手而不是动态创建闭包,那么
答案 0 :(得分:7)
您在此处看到的费用根本不与“关闭”相关联,而是与Stream
初始化的费用相关联。
我们来看看你的三个示例代码:
for (int i = 0; i < 10_000_000; i++) {
Set<String> set = Collections.emptySet();
set.stream().forEach(s -> System.out.println(s));
}
这个在每个循环中创建一个 new 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循环,因为10k执行是JIT需要启动的(至少,HotSpot以这种方式行动)。
现在,lambdas:它们是呼叫站点,它们只链接一次;但是,当然,初始链接的成本仍然存在,并且在您的循环中,包含此成本。在进行测量之前,请尝试并仅运行一个循环,这样可以避免这种成本。
总而言之,这不是一个有效的微基准测试。使用caliper或jmh来真正衡量性能。
观看lambdas如何运作的精彩视频here。它现在有点老了,JVM比lambdas好多了。
如果您想了解更多信息,请查找有关invokedynamic
的文献。