【问题标题】:Groovy: Is for..in significantly faster than .each?Groovy:for..in 比 .each 快得多吗?
【发布时间】:2015-06-05 08:38:33
【问题描述】:

出于性能原因,我很好奇 for..in 是否应该优先于 .each

【问题讨论】:

  • 到目前为止您尝试过什么?请注意,JMH (openjdk.java.net/projects/code-tools/jmh) 有一个 groovy 模板,这使得在 groovy 中创建基准测试变得容易(但不是微不足道的)。请务必返回您的发现!
  • 对我来说,将所有 each 更改为 for-in 可能是优化技术列表中的最后一个。只是说 :-) 或者换句话说,一个不必要的数据库调用可以等于 1000 个优化的for-ins
  • 这真的是您应用程序的瓶颈吗?
  • 它(真的)重要吗?

标签: groovy microbenchmark


【解决方案1】:

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 每个{}
  • 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

结果:

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

【讨论】:

  • 这是四年前的数据。永远不要相信你自己没有操纵的基准。
  • 你是对的,但它链接了一个要点,每个人都可以再次运行基准测试。我只是用 java8 做的,结果是:3056480 / 8181870 / 3327981 / 320949
  • 列表有多长?
  • 列表大小为 66 ,很短。
  • “在每个代码块内你不能使用 break 和 continue 语句”——这是错误的。有一个理解语言和理解为什么那些不做一些人认为他们在这种情况下在闭包内做的事情的问题,但这是完全合法的,并且有充分的理由在里面使用 breakcontinue传递给each 方法的闭包。例如,如果你在闭包中有一个循环或一个 switch 语句。
【解决方案2】:

让我们从理论上看一下哪些调用是动态的,哪些调用是用 Java 逻辑更直接地完成的(我将这些调用称为静态调用)。

for-in 的情况下,Groovy 对一个迭代器进行操作,为了获得它,我们对 iterator() 进行了一次动态调用。如果我没记错的话,hasNext 和 next 调用是使用普通的 Java 方法调用逻辑完成的。因此,对于每次迭代,我们这里只有 2 个静态调用。根据基准,必须注意第一次 iterator() 调用可能会导致严重的初始化时间,因为这可能会初始化元类系统并且需要一些时间。

each 的情况下,我们对每个自身进行动态调用,以及为打开的块创建对象(闭包实例)。 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 循环最快。 gist.github.com/frhack/5db0485f9847e6b673be
  • @frhack 你必须小心这样的微基准测试。你必须给它一个适当的预热时间,最好是同时只测试一个构造。我很肯定,如果在这些限制条件下进行测试,两者的时间非常相似。但是代码大小当然存在差异。 forEach 版本具有 for-each 方法以及必须优化的 lambda。普通的 for 循环版本有循环体,也有循环体优化的方法。这就是为什么在测试中将循环与其他所有内容放在一起是不好的。
  • 它会导致要优化的部分代码量很大,并且可能会使编译器不这样做。最后,forEach 执行与正常循环相同的逻辑步骤,但中间有更多的方法调用。所以优化需要更长的时间......但最终两者应该是相等的。其他一切对我来说都意味着浪费了潜力。对我来说,您的示例没有任何技术原因应该表现得如此不同
猜你喜欢
  • 1970-01-01
  • 2020-01-26
  • 2023-03-29
  • 1970-01-01
  • 2010-11-17
  • 2022-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多