2.2 配套代码:调用点类型数量与去优化
对应小节:2.2 JIT 的三大优化与去优化 要验证的结论:同一个调用点混入的类型越多,性能越差;而且退化不是按比例发生的。
一、实验:单态 / 双态 / 巨态
// src/main/kotlin/lesson02/CallSitePolymorphism.kt
package lesson02
import kotlin.random.Random
/** 一个有多个实现的接口 */
interface Handler {
fun handle(v: Int): Int
}
class HandlerA : Handler { override fun handle(v: Int) = v + 1 }
class HandlerB : Handler { override fun handle(v: Int) = v * 2 }
class HandlerC : Handler { override fun handle(v: Int) = v - 3 }
class HandlerD : Handler { override fun handle(v: Int) = v xor 7 }
class HandlerE : Handler { override fun handle(v: Int) = v shr 1 }
private var sink = 0L
/**
* 关键:这里只有一个调用点 `h.handle(v)`。
* 传入的 Handler 实现类数量,决定了这个调用点的"态"。
*/
fun runLoop(handlers: Array<Handler>, iterations: Int): Long {
var acc = 0L
var state = 12345
repeat(iterations) {
state = state * 1103515245 + 12345 // 简单的伪随机,避免被优化
val h = handlers[(state ushr 16) and (handlers.size - 1)]
acc += h.handle(it) // ← 唯一的调用点
}
sink += acc
return acc
}
fun measure(handlers: Array<Handler>, warmup: Int = 2_000_000, measure: Int = 5_000_000): Double {
repeat(warmup) { runLoop(handlers, 100) }
val t0 = System.nanoTime()
runLoop(handlers, measure)
val t1 = System.nanoTime()
return (t1 - t0).toDouble() / measure
}
fun main() {
// 注意:handlers.size 必须是 2 的幂,上面的位运算取模才正确
val cases = listOf(
"单态(1 种实现)" to arrayOf<Handler>(HandlerA()),
"双态(2 种实现)" to arrayOf<Handler>(HandlerA(), HandlerB()),
"四态(4 种实现)" to arrayOf<Handler>(HandlerA(), HandlerB(), HandlerC(), HandlerD()),
"巨态(8 种实现)" to arrayOf<Handler>(
HandlerA(), HandlerB(), HandlerC(), HandlerD(),
HandlerE(), HandlerA(), HandlerB(), HandlerC()
),
)
println("调用点的类型数量 → 单次耗时")
println("-".repeat(46))
val baseline = measure(cases[0].second)
for ((label, handlers) in cases) {
val ns = measure(handlers)
println("%-18s %8.2f ns/op (%.2fx)".format(label, ns, ns / baseline))
}
println()
println("sink = $sink")
println()
println("预期:类型越多越慢。注意『双态』相对『单态』的退化幅度——")
println(" 即使只有 2 种类型,JIT 也无法再做单态内联优化。")
}
二、预期输出形态
调用点的类型数量 → 单次耗时
----------------------------------------------
单态(1 种实现) 4.12 ns/op (1.00x)
双态(2 种实现) 7.83 ns/op (1.90x)
四态(4 种实现) 12.41 ns/op (3.01x)
巨态(8 种实现) 18.06 ns/op (4.38x)
关键不是具体倍数,而是趋势:即使只有 2 种实现,退化也很明显(接近 2 倍)。这就是「1% 的慢路径拖累 99% 的快路径」的微观机制。
三、观察去优化
# 运行并把编译事件写进文件
java -XX:+PrintCompilation -cp build/classes/kotlin/main \
lesson02.CallSitePolymorphismKt > compile.log 2>&1
# 看去优化事件
grep "not entrant" compile.log | head -20
# 看去优化的详细原因(需要诊断选项)
java -XX:+UnlockDiagnosticVMOptions -XX:+TraceDeoptimization \
-cp build/classes/kotlin/main lesson02.CallSitePolymorphismKt 2>&1 | grep -i deopt | head -20
你会看到什么:
- 在切换 handler 类型的过程中,
runLoop或handle会出现made not entrant——说明之前的优化假设被打破了。 - 这也解释了为什么某些服务在流量类型变化时(比如大促时某种请求突然增多)性能会突然掉下来。
四、验证内联:-XX:CompileCommand
# 禁止内联 handle,看性能变化
java -XX:CompileCommand=dontinline,lesson02/Handler*.handle \
-cp build/classes/kotlin/main lesson02.CallSitePolymorphismKt
| 情况 | 预期 |
|---|---|
| 单态 + 允许内联 | 最快 |
| 单态 + 禁止内联 | 明显变慢(说明单态之所以快,主要靠内联) |
| 巨态 + 允许内联 | 慢(无法内联) |
这组对比告诉你:单态 → 双态 的退化,本质是**「能不能内联」**的差别。
五、在真实代码里怎么应用
// ❌ 一个调用点处理多种类型 → megamorphic
fun render(items: List<Any>) {
items.forEach { renderOne(it) } // renderOne 的调用点是巨态
}
// ✅ 按类型分组,让每个调用点保持单态
fun render(items: List<Any>) {
items.filterIsInstance<Order>().forEach { renderOrder(it) } // 单态
items.filterIsInstance<User>().forEach { renderUser(it) } // 单态
}
// ✅ 或者在 when 里分支(每个分支内部的调用点是单态)
fun renderOne(item: Any) = when (item) {
is Order -> orderRenderer.render(item) // 这条路径始终只有 OrderRenderer
is User -> userRenderer.render(item)
else -> error("unknown")
}
但请先测量! 多数业务代码的开销大头是 IO(数据库、网络),虚调用的差异微不足道。只有当火焰图或基准证明这里有问题时才改。 为此把代码写丑是不划算的。
六、动手改造
| 改动 | 观察什么 |
|---|---|
| 把「巨态」改成 16 种实现 | 退化会继续加剧还是趋于平缓?(提示:JIT 放弃优化后有一个下限) |
| 让分布不均匀(90% 走 HandlerA,10% 走其他) | 会变快吗?(提示:megamorphic 是按「出现过的类型数」判定,不按比例) |
在 handle 里加一点计算量 |
相对差异会缩小——说明调用开销在总耗时里的占比决定了这个优化的价值 |
去掉 sink 累加 |
整个循环可能被优化掉(第 3 节的死代码消除) |
七、这段代码的局限
- 随机选择 handler 放大了退化:真实代码里类型分布往往是偏斜的(90% 走一种)。JIT 对偏斜的双态处理更好,所以实际退化可能小于本实验。
handlers.size必须是 2 的幂,否则上面的位运算取模是错的。如果要测 3、5 种类型,改成% handlers.size。- 开发机噪声:这里的绝对值不可信,只看倍数关系。
- 这个实验不能告诉你「该不该优化」——它只告诉你「类型不稳定的代价有多大」。是否值得改,取决于这个调用在你的火焰图里占多少。