JIT 编译原理

Just-In-Time 编译——让解释型语言跑出接近编译型语言的性能。

概念

JIT (Just-In-Time) 编译是在程序运行时将字节码(或源码)编译为机器码的技术。与 AOT (Ahead-Of-Time) 编译不同,JIT 拥有运行时信息(类型反馈、执行频率、分支概率),可以做”投机优化”(Speculative Optimization),在很多场景下甚至超过 AOT 编译的性能(如 Java 长服务进程)。

核心分类

类型触发条件优点缺点代表
Method JIT检测到某个方法调用频率超过阈值编译单元清晰, 优化范围大冷函数编译浪费HotSpot C1/C2
Tracing JIT检测到某个循环路径热度高只编译热路径, 开销低边角情况需要 guardTraceMonkey, LuaJIT
Baseline JIT所有代码首次执行时 JIT没有解释器开销编译压力大SpiderMonkey Baseline

Method JIT vs Tracing JIT

// Method JIT (HotSpot):
//   编译整个方法体, 不管冷热分支, 覆盖所有 case
function compute(a, b, op):
    IF op == 1:  return a + b        // 热路径
    IF op == 2:  return a - b        // 冷路径
    IF op == 3:  return a * b        // 冷路径
    IF op == 4:  return a / b        // 冷路径
    // ^ 全部编译为机器码

// Tracing JIT (LuaJIT):
//   只追踪实际执行的热路径, 编译为一条直线代码
//   op == 1 路径: a + b 的机器码
//   路径入口有 guard 检查 op == 1
//   如果 op != 1, guard 失败 → 跳回解释器

内联缓存 (Inline Caching)

// 无优化: 每次方法调用都走虚表查找
obj.toString()  // Mask: load obj.klass → load klass.vtable[toString_slot] → call

// 内联缓存 (IC):
// 缓存最近一次的类型和对应的函数指针
if (obj.klass == cached_klass) {
    call cached_toString_impl;   // 快速路径
} else {
    // 慢速路径: 查找 vtable, 更新缓存
    cached_klass = obj.klass;
    cached_toString_impl = lookup(obj.klass, "toString");
    call cached_toString_impl;
}

// 多态内联缓存 (PIC): 缓存 3-4 个常见类型的函数指针
// Megamorphic: 超过阈值则退回 vtable 查找

投机优化 (Speculative Optimization)

// 基于运行时 profile 的投机优化

// 场景: 函数总是接收 int 参数
function add(a, b) { return a + b; }

// TurboFan 生成以下伪机器码:
//   ...
//   CHECK a IS Smi          ← guard
//   CHECK b IS Smi          ← guard
//   temp = a + b            ← 已优化为单条 CPU 加法指令
//   CHECK overflow          ← guard
//   RETURN temp
//   ...
//   DEOPTIMIZE:             ← 任一个 guard 失败即跳转
//       保存状态
//       丢弃编译的机器码
//       从 Ignition 字节码继续执行

// 去优化 (Deoptimization):
//   1. 记录 recompile 信息 (哪类 guard 失败, 常见类型是什么)
//   2. 下次编译时根据新的类型反馈生成新的机器码
//   3. 这就是为什么 V8 可能对一个函数生成多个版本

分层编译阶梯

性能
  ^
  |                                Level 4: C2 / TurboFan
  |                                  深度优化机器码
  |                                    - 内联展开
  |                                    - 逃逸分析
  |                                    - 循环展开
  |                                    - SIMD 向量化
  |                                    - 分支预测
  |
  |                      Level 3: C1 full profile / Ignition + feedback
  |                        收集所有类型反馈
  |
  |           Level 2: C1 limited profile
  |             收集部分类型反馈
  |
  |  Level 1: C1 quick / Ignition
  |    无反馈, 快速编译
  |
  |  Level 0: 纯解释
  +----------------------------------------------------------> 时间

PGO (Profile-Guided Optimization)

AOT 编译器也可以利用 PGO 模拟 JIT 的效果:

1. 在程序带有 profiling 选项的情况下运行 → 生成 .profdata
2. 重新编译, 编译器读取 .profdata
3. 编译器根据 profile 信息做代码布局 (热路径放一起)、内联决策

JIT 拥有运行时实时 profile, 但占用内存和 CPU 资源; PGO 不占运行时资源, 但需要离线采集, 无法应对部署后的行为变化。