我注意到Itarable.sum()
的性能与手动求和的直接循环之间存在惊人的差异。考虑一下:
import kotlin.system.measureTimeMillis
fun main(args: Array<String>) {
var sink = 0;
repeat(5) {
println(measureTimeMillis {
var sum = 0
for (i in 1..10_000_000) {
sum += i
}
sink += sum
})
}
repeat(5) {
println(measureTimeMillis {
sink += (1..10_000_000).sum()
})
}
}
令人惊讶的是,使用Iterable.sum()
的速度要慢 10 倍, 与与 sum(( 实现几乎相同的代码相比。 为什么?
更新:
当我以 js 为目标时,sum(( 只是稍微慢一点。
measureTimeMillis()
可以定义为:
import kotlin.js.Date
public inline fun measureTimeMillis(block: () -> Unit): Double {
val start = Date.now()
block()
return Date.now() - start
}
更新2:
在同一台 Linux 机器上,jvm sum(( 甚至比 js 慢。以下是 jvm (Oracle jdk9( 和 js(最新 chrome( 的 100_000_000 次迭代的结果:
105 // jvm raw loop
76 // jvm raw loop (jit?)
75 // jvm raw loop (jit?)
75 // jvm raw loop (jit?)
70 // jvm raw loop (jit?)
633 // jvm sum()
431 // jvm sum()
562 // jvm sum()
327 // jvm sum() (jit?)
332 // jvm sum() (jit?)
110 // js raw loop
108 // js raw loop
232 // js raw loop
227 // js raw loop
227 // js raw loop
321 // js sum()
284 // js sum()
264 // js sum()
266 // js sum()
265 // js sum()
因此,在同一台机器上,jvm在使用sum()
时似乎比js慢。又是一个惊喜。
显然,我们在这里比较的是超优化的紧密循环。在"内置"情况下,我在"手动总和"和巨大方差的重复中看到相当稳定的结果。这表示气相色谱活性。
在启动 VisualVM 并使用其 VisualGC 插件时,我确认在手动求和计算过程中没有 GC 活动,但在内置案例中
有很多活动。查看生成的字节码,差异变得明显:成语for (i in 1..range) { ... }
直接编译成计数循环。这实际上是有记录的:
积分类型范围(
IntRange
、LongRange
、CharRange
(有一个额外的特性:它们可以迭代。编译器负责将其类似地转换为 Java 的索引 for 循环,而不会产生额外的开销。
不幸的是,相同的优化不适用于扩展函数Iterable.sum()
因为它必须适用于任何Iterable
。编译器可以看到发生了什么,并引入另一个内联函数,它会简单地将整个事情转换为结果总和而无需计算,或者如果范围边界没有硬编码,则使用直接公式。
JavaScript在这里也处于类似的立场,因为它也有一个强大的JIT编译器。我不能评论任何具体的东西,但它很可能避免了热循环中的分配。