文档目录

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。
  • 开发机噪声:这里的绝对值不可信,只看倍数关系。
  • 这个实验不能告诉你「该不该优化」——它只告诉你「类型不稳定的代价有多大」。是否值得改,取决于这个调用在你的火焰图里占多少。