为什么 Iterable.sum() 在 Kotlin 中很慢?
Why Iterable.sum() is slow in Kotlin?
我注意到 Itarable.sum()
的性能与使用手动求和的直接 for 循环之间的惊人差异。考虑一下:
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() implementation几乎相同的代码相比。
这是为什么?
更新:
当我以 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()
所以,在同一台机器上,使用 sum()
时,jvm 似乎比 js 慢。又是一个惊喜。
显然,我们在这里比较的是超级优化的紧密循环。我在 "manual sum" 的重复和 "built-in" 的情况下看到了非常稳定的结果。这表示 GC activity.
启动 VisualVM 并使用其 VisualGC 插件后,我确认在手动总和计算期间没有 GC activity,但在内置案例中有很多。
查看生成的字节码,区别就很明显了:惯用法 for (i in 1..range) { ... }
直接编译成一个计数循环。这实际上是 documented:
Integral type ranges (IntRange
, LongRange
, CharRange
) have an extra feature: they can be iterated over. The compiler takes care of converting this analogously to Java's indexed for-loop, without extra overhead.
不幸的是,相同的优化不适用于扩展函数 Iterable.sum()
,因为它必须适用于任何 Iterable
。编译器 可以 看到发生了什么并引入另一个内在函数,它可以简单地将整个事物转换为结果总和而无需计算,或者如果范围边界没有硬编码则使用直接公式。
JavaScript 在这里处于类似的地位,因为它也有一个强大的 JIT 编译器。我不能评论任何具体的东西,但它很可能避免在热循环中分配。
我注意到 Itarable.sum()
的性能与使用手动求和的直接 for 循环之间的惊人差异。考虑一下:
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() implementation几乎相同的代码相比。
这是为什么?
更新:
当我以 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()
所以,在同一台机器上,使用 sum()
时,jvm 似乎比 js 慢。又是一个惊喜。
显然,我们在这里比较的是超级优化的紧密循环。我在 "manual sum" 的重复和 "built-in" 的情况下看到了非常稳定的结果。这表示 GC activity.
启动 VisualVM 并使用其 VisualGC 插件后,我确认在手动总和计算期间没有 GC activity,但在内置案例中有很多。
查看生成的字节码,区别就很明显了:惯用法 for (i in 1..range) { ... }
直接编译成一个计数循环。这实际上是 documented:
Integral type ranges (
IntRange
,LongRange
,CharRange
) have an extra feature: they can be iterated over. The compiler takes care of converting this analogously to Java's indexed for-loop, without extra overhead.
不幸的是,相同的优化不适用于扩展函数 Iterable.sum()
,因为它必须适用于任何 Iterable
。编译器 可以 看到发生了什么并引入另一个内在函数,它可以简单地将整个事物转换为结果总和而无需计算,或者如果范围边界没有硬编码则使用直接公式。
JavaScript 在这里处于类似的地位,因为它也有一个强大的 JIT 编译器。我不能评论任何具体的东西,但它很可能避免在热循环中分配。