Groovy:使用for..in循环是否比.each方法更快?

8

我很好奇在性能方面是否应该优先使用for..in而不是.each


2
你目前尝试了什么?请注意,JMH(http://openjdk.java.net/projects/code-tools/jmh/)有一个Groovy模板,可以轻松(但不是微不足道的)创建Groovy基准。一定要分享你的发现! - llogiq
6
将所有的 each 改为 for-in 对我来说可能是优化技术清单上最后一个要考虑的。只是这么说 :-) 或者换句话说,一次不必要的数据库调用可能相当于数千个经过优化的 for-in 循环。 - defectus
6
这真的是你的应用程序中的瓶颈吗? - user1038550
2
相关链接:https://dev59.com/MYDba4cB1Zd3GeqPCEZF - jalopaba
2
这(真的)很重要吗? - john
显示剩余2条评论
2个回答

5
For .. in是标准的语言流程控制之一。
相比之下,each调用一个闭包会增加额外的开销。 .each {...}是语法糖,相当于方法调用.each({...})
此外,由于它是闭包,在each代码块中你不能使用breakcontinue语句来控制循环。 http://kunaldabir.blogspot.it/2011/07/groovy-performance-iterating-with.html 更新的基准测试Java 1.8.0_45 Groovy 2.4.3:
  • 3327981 each {}
  • 320949 for(){
这里是另一个100000次迭代的基准测试:
lines = (1..100000)
// with list.each {}
start = System.nanoTime()
lines.each { line->
    line++;
}
println System.nanoTime() - start

// with loop over list
start = System.nanoTime()
for (line in lines){
    line++;
}
println System.nanoTime() - start

results:

  • 261062715 each{}
  • 64518703 for(){}

2
这是四年前的数据。永远不要相信你没有操纵过的基准测试。 - cfrick
“在每个代码块中,您不能使用break和continue语句” - 这是错误的。在闭包内部使用breakcontinue存在理解语言和理解它们在该上下文中不起作用的问题,但这是完全合法的,并且有使用breakcontinue传递给each方法的闭包的有效原因。例如,如果在闭包内部有一个循环或switch语句。 - Jeff Scott Brown
很多时候,当有人得出结论说breakcontinue在那里不起作用时,这表明他们误解了将闭包作为参数传递给each方法时发生的情况。如果你编写了一个包含continue的方法,并且你从循环内部调用该方法,那么你期望continue是什么意思?这就类似于在传递给each方法的闭包内部使用continue时发生的情况。 - Jeff Scott Brown
显然,我们无法使用break和continue来修改闭包中的循环,就像.each{}这样的内部代码块一样。 显然,在闭包中,我们可以创建循环并使用break和continue。 - frhack
说实话,在我的看法中,我们不能在闭包块代码的循环中使用break和continue是非常好的。它强制我们以正确的方式使用这些结构:λ演算的方式。 - frhack
显示剩余3条评论

5
让我们从理论上看一下调用哪些是动态的,哪些是更直接使用Java逻辑的静态调用(我将称之为静态调用)。
在for-in的情况下,Groovy操作一个迭代器,为了得到它,我们需要一次动态调用iterator()。如果我没记错,hasNext和next调用是使用普通的Java方法调用逻辑完成的。因此,在每次迭代中,我们只有2个静态调用。根据基准测试,必须注意的是,第一个iterator()调用可能会导致严重的初始化时间,因为那可能会初始化元类系统,这需要一段时间。
在each的情况下,我们有对each本身的动态调用,以及开放块(Closure的实例)的对象创建。each(Closure)也会调用iterator(),但未缓存...所有一次性费用。在循环期间,hasNext和next使用Java逻辑完成,这使得有2个静态调用。对Closure实例的调用是使用java标准逻辑进行方法调用,然后将使用动态调用调用doCall。
总之,每次迭代,for-in只使用2个静态调用,而each具有3个静态调用和1个动态调用。动态调用比多个静态调用慢得多,而且更难为JVM优化,因此支配着时间。因此,只要开放块需要动态调用,each就应该始终更慢。
由于Closure#call的复杂逻辑,很难将动态调用优化掉。这很烦人,因为它实际上并不需要,并且一旦我们找到解决方法,它将被删除。如果我们成功了,each可能仍然更慢,但这是一个更困难的问题,因为字节码大小和调用配置起到了作用。理论上它们可以相等(忽略初始化时间),但JVM需要做更多的工作。当然,同样适用于例如Java8中基于流的lambda处理的示例。

在Java8中,使用闭包的forEach似乎比标准的for循环更快。 - frhack
@frhack在进行微基准测试时一定要小心。你必须给它足够的预热时间,并且最好一次只测试一个结构。如果在这些限制下进行测试,我相信两者的时间非常相似。但是,代码大小当然存在差异。forEach版本有for-each方法以及必须优化的lambda表达式。普通for-loop版本有循环体,但也有循环体所在的方法需要优化。这就是为什么将循环体直接放在测试中并不好的原因。 - blackdrag
这会导致需要优化的部分代码量很大,可能会使编译器不进行优化。最终,forEach与普通循环执行相同的逻辑步骤,但在它们之间调用了更多的方法。因此,它需要更长的时间来进行优化...但最终两者应该是相等的。对我来说,其他任何情况都意味着浪费潜力。对我来说,你的例子没有什么技术原因会表现得截然不同。 - blackdrag

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接