一句话核心结论 :同一个 fib(40) 函数,C++ AOT 编译需要 5ms,V8 JIT 需要 30ms,CPython 解释执行需要 3000ms——600 倍差距是怎么来的? 本文用 200 行 C++17 手写一个简单 JIT,跑通解释 → 字节码 → JIT 全链路,让你看到 JIT 编译器每一个关键决策背后的工程权衡。
系列完结 · 全 5 篇 前言:从 AOT 到 JIT,编译时机的一次革命 为什么需要 JIT? AOT(Ahead-of-Time) 编译在程序运行前完成所有编译工作,输出可执行文件;JIT(Just-in-Time) 编译在程序运行过程中实时编译热点代码。
graph LR
subgraph "AOT 模式"
A1["源码"] --> A2["编译期<br/>编译成机器码"] --> A3["可执行文件"] --> A4["运行期<br/>直接执行"]
end
subgraph "JIT 模式"
B1["字节码"] --> B2["运行期<br/>解释执行"] --> B3["热点检测"] --> B4["JIT 编译<br/>→ 机器码"] --> B5["执行机器码"]
end
style A1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style A2 fill:#E8D5F5,stroke:#CE93D8,color:#333
style A3 fill:#FFF9C4,stroke:#F9A825,color:#333
style A4 fill:#B5EAD7,stroke:#80CBC4,color:#333
style B1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B2 fill:#FFDAB9,stroke:#FFAB76,color:#333
style B3 fill:#FFF9C4,stroke:#F9A825,color:#333
style B4 fill:#E8D5F5,stroke:#CE93D8,color:#333
style B5 fill:#B5EAD7,stroke:#80CBC4,color:#333核心命题 :JIT 把”什么时候编译”从”程序员选择”变成”运行时自适应” ——它能在不知道输入数据的情况下,根据实际运行行为做出最优的优化决策。
JIT 的 4 大优势 优势 AOT 的痛点 JIT 的解法 运行时类型信息 编译时不知道实际类型,必须保守优化 JIT 看到 obj.x 一直是 Point,直接生成 Point.x 的字段访问 运行时分支 profile 编译时假设 if 各 50% 概率 JIT 看到 99% 进 then 分支,倒置跳转 延迟编译 所有代码一视同仁 只编译热点(10% 代码跑 90% 时间) 动态去优化 一旦生成机器码,无法回退 发现假设错误,OSR 回退到解释器 重编译
三大 JIT 流派概览 流派 思想 代表 触发单位 Method-based JIT 以方法 为编译单位,热点方法整段编译 HotSpot C1/C2 方法调用计数 Tracing JIT 以循环/热路径 为编译单位,记录实际执行路径 LuaJIT、PyPy 循环回边计数 Baseline + Optimizing 双层:先低级编译,再升级高级优化 V8 Sparkplug + TurboFan 方法调用计数
本文目标 :手写一个 Mini JIT(约 200 行),覆盖以上三大流派的核心思想 ;并解释 HotSpot/V8/LuaJIT 的真实设计。
一、JIT 编译全景:什么时候编译、编译什么、怎么编译 1.1 三大执行模型对比 graph TB
subgraph "解释执行"
I1["源码 / 字节码"] --> I2["主循环<br/>读取指令<br/>分发到 handler"] --> I3["执行<br/>每条指令 ~10-100ns"]
end
subgraph "字节码 + 简单 JIT"
J1["字节码"] --> J2{"方法调用次数<br/>> 阈值?"}
J2 -->|"否"| J3["解释执行"]
J2 -->|"是"| J4["JIT 编译<br/>方法级编译"] --> J5["执行机器码"]
end
subgraph "Tracing JIT"
T1["字节码"] --> T2{"循环回边<br/>> 阈值?"}
T2 -->|"否"| T3["解释执行"]
T2 -->|"是"| T4["记录热路径<br/>(trace)"] --> T5["编译 trace<br/>线性无分支"] --> T6["执行机器码"]
end
style I1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style I2 fill:#E8D5F5,stroke:#CE93D8,color:#333
style I3 fill:#FFDAB9,stroke:#FFAB76,color:#333
style J1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style J2 fill:#FFF9C4,stroke:#F9A825,color:#333
style J3 fill:#FFDAB9,stroke:#FFAB76,color:#333
style J4 fill:#E8D5F5,stroke:#CE93D8,color:#333
style J5 fill:#B5EAD7,stroke:#80CBC4,color:#333
style T1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style T2 fill:#FFF9C4,stroke:#F9A825,color:#333
style T3 fill:#FFDAB9,stroke:#FFAB76,color:#333
style T4 fill:#E8D5F5,stroke:#CE93D8,color:#333
style T5 fill:#E8D5F5,stroke:#CE93D8,color:#333
style T6 fill:#B5EAD7,stroke:#80CBC4,color:#3331.2 AOT / 解释 / JIT 三种执行方式 维度 AOT(C++/Rust) 解释(CPython) JIT(V8/HotSpot) 编译时机 运行前 不编译 运行中 启动速度 快(直接执行) 慢(无需编译) 中(要预热) 峰值性能 ⭐⭐⭐⭐⭐ ⭐ ⭐⭐⭐⭐ 类型信息 静态类型 动态类型 + 运行时检查 运行时 profile 内存占用 小(仅机器码) 小(仅字节码) 大(字节码 + JIT 代码 + profile) 平台无关 ❌(需重新编译) ✅ ✅(输出机器码,但与 OS 绑定) 典型场景 系统程序、性能敏感 脚本、胶水代码 长跑服务端、桌面应用
1.3 JIT 触发的 3 个条件 flowchart TD
START(["方法入口 / 循环回边"]) --> COUNT{"调用/回边次数<br/>超过阈值?"}
COUNT -->|"否"| INTERP["继续解释"]
COUNT -->|"是"| PROFILE{"profile 数据足够?<br/>(分支、类型)"}
PROFILE -->|"否"| QUEUE["排队等编译<br/>后台线程"]
PROFILE -->|"是"| COMPILE["JIT 编译<br/>生成机器码"]
COMPILE --> PATCH["代码桩替换<br/>(code patching)"]
PATCH --> NEXT["下次调用<br/>跳到机器码"]
style START fill:#C7CEEA,stroke:#9FA8DA,color:#333
style COUNT fill:#FFF9C4,stroke:#F9A825,color:#333
style INTERP fill:#FFDAB9,stroke:#FFAB76,color:#333
style PROFILE fill:#FFF9C4,stroke:#F9A825,color:#333
style QUEUE fill:#FFDAB9,stroke:#FFAB76,color:#333
style COMPILE fill:#E8D5F5,stroke:#CE93D8,color:#333
style PATCH fill:#E8D5F5,stroke:#CE93D8,color:#333
style NEXT fill:#B5EAD7,stroke:#80CBC4,color:#333触发条件 触发方式 典型阈值 代表 方法调用计数 方法头插入计数器 +N 10000~15000 次 HotSpot C1 循环回边计数 循环末尾 +N 10700 次(OSR 入口) HotSpot C2 采样(sampler) 后台线程周期性栈采样 100Hz HotSpot 旧版
OSR(On-Stack Replacement) :当循环回边计数超阈值时,当前正在执行的循环需要被替换 ——栈上替换技术让正在跑的字节码”瞬间”变成机器码,且变量值、调用栈不丢失 。
1.4 分层编译(Tiered Compilation) graph LR
A["🔵 Tier 0<br/>解释执行<br/>+ 插桩"] -->|调用 1500 次| B["🟣 Tier 1<br/>C1 简单编译<br/>无 profile"]
B -->|调用 15000 次| C["🟢 Tier 2<br/>C1 + profile"]
C -->|调用 100000 次| D["🟠 Tier 3<br/>C2 激进优化<br/>基于 profile"]
D -->|假设失败| E["🔴 Deopt<br/>回退到解释器"]
E -.->|重编译| D
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
style D fill:#FFDAB9,stroke:#FFAB76,color:#333
style E fill:#FFB3C6,stroke:#F48FB1,color:#333层级 编译器 编译时间 优化 何时使用 Tier 0 解释器 0 无 启动期 Tier 1 C1 简单 ~1ms 简单 调用 1500 次 Tier 2 C1 + profile ~5ms profile 收集 调用 15000 次 Tier 3 C2 ~100ms 全优化 调用 100000 次 Tier 4 C2 (OSR) ~100ms 全优化 + OSR 循环回边
关键观察 :C1 快但糙,C2 慢但精 ——分层编译让”启动速度”和”峰值性能”兼得。
二、解释器实现:JIT 的”前半生” 2.1 三种解释器架构 类型 思路 代表 性能 AST 解释器 直接遍历 AST 执行 第 1 篇的 mini 解释器 ⭐ 慢 字节码解释器 先编译成字节码,再解释 CPython、Lua 5.1 ⭐⭐⭐ 寄存器虚拟机 操作数在寄存器中(数组模拟) LuaJIT、Dalvik ⭐⭐⭐⭐ 栈式虚拟机 操作数在栈上 JVM、CPython、HotSpot ⭐⭐⭐
2.2 字节码 vs AST 解释器 graph TB
subgraph "AST 解释器"
A1["AST 节点<br/>遍历 = 指针跳转"] --> A2["每个节点<br/>~50ns 开销<br/>(虚函数、cache miss)"]
end
subgraph "字节码解释器"
B1["字节码<br/>[LOAD, PUSH, ADD, ...]"] --> B2["主循环<br/>switch opcode<br/>~5ns/指令"]
end
style A1 fill:#FFDAB9,stroke:#FFAB76,color:#333
style A2 fill:#FFB3C6,stroke:#F48FB1,color:#333
style B1 fill:#B5EAD7,stroke:#80CBC4,color:#333
style B2 fill:#B5EAD7,stroke:#80CBC4,color:#333性能差 10x 的原因 :AST 解释器每条语句有 2-3 层虚函数调用 、指针追逐 、cache miss ;字节码解释器是紧凑数组 + switch 跳转 ,CPU 预取友好。
2.3 字节码设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 enum class Op : uint8_t { PUSH_INT, LOAD, STORE, ADD, SUB, MUL, DIV, LT, GT, EQ, NEQ, JMP, JMP_IF_FALSE, CALL, RET, PRINT, }; struct Instr { Op op; int32_t operand; }; struct Function { std::string name; std::vector<Instr> code; int n_params; int n_locals; };
2.4 寄存器虚拟机 vs 栈式虚拟机 维度 栈式(Stack-based) 寄存器(Register-based) 指令长度 短(无操作数) 长(需指定寄存器) 指令数 多(每步都 push/pop) 少(直接操作) 解释开销 高(频繁 push/pop) 低(直接寄存器访问) JIT 友好度 ⭐⭐⭐ ⭐⭐⭐⭐ 代表 JVM、CPython LuaJIT、Dalvik
对比 a + b * c 的字节码 :
1 2 3 4 5 6 7 8 9 10 栈式(JVM): ILOAD a # push a ILOAD b # push b ILOAD c # push c IMUL # pop b, c; push b*c IADD # pop a, (b*c); push a+b*c 寄存器(LuaJIT): MUL r1, r_b, r_c ADD r0, r_a, r1
2.5 AST 解释器 → 字节码解释器(升级 #1 mini 编译器) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class BytecodeCompiler { std::vector<Instr> code_; std::map<std::string, int > locals_; int next_slot_ = 0 ; void gen_expr (const Expr& e) { if (auto * lit = dynamic_cast <const IntLit*>(&e)) { code_.push_back ({Op::PUSH_INT, (int32_t )lit->value}); } else if (auto * v = dynamic_cast <const VarRef*>(&e)) { code_.push_back ({Op::LOAD, locals_[v->name]}); } else if (auto * b = dynamic_cast <const Binary*>(&e)) { gen_expr (*b->lhs); gen_expr (*b->rhs); Op op = parse_binop (b->op); code_.push_back ({op, 0 }); } } };
性能对比 (实测,第 1 篇 AST 解释器跑 fib(30) ~1500ms,本节字节码 ~300ms)—— 5x 加速 ,且代码量更小。
三、Method-based JIT:HotSpot C1/C2 的核心 3.1 HotSpot 的 4 个编译器 graph TB
SRC["Java 源码<br/>.java"] --> BYTECODE["Java 字节码<br/>.class"]
BYTECODE --> C1["🟣 C1 客户端编译器<br/>(快速编译)"]
BYTECODE --> C2["🟠 C2 服务端编译器<br/>(激进优化)"]
BYTECODE --> INTERP["🔵 解释器"]
C1 --> C1_CODE["C1 机器码<br/>+ profile"]
C1_CODE --> C2
C2 --> C2_CODE["C2 机器码<br/>(高度优化)"]
style SRC fill:#C7CEEA,stroke:#9FA8DA,color:#333
style BYTECODE fill:#FFDAB9,stroke:#FFAB76,color:#333
style INTERP fill:#E8D5F5,stroke:#CE93D8,color:#333
style C1 fill:#B5EAD7,stroke:#80CBC4,color:#333
style C2 fill:#FFB3C6,stroke:#F48FB1,color:#333
style C1_CODE fill:#B5EAD7,stroke:#80CBC4,color:#333
style C2_CODE fill:#FFB3C6,stroke:#F48FB1,color:#3333.2 C1 vs C2 编译器对比 维度 C1(Client Compiler) C2(Server Compiler) 目标 启动快 峰值性能 编译时间 ~1ms ~100ms 优化激进度 简单(无 profile) 激进(基于 profile) 方法内联深度 浅(1-2 层) 深(10+ 层) 逃逸分析 基础 完整 循环展开 ❌ ✅ 向量化 ❌ ✅ 去优化支持 简单 完整 代表调用 -client-server(默认)
3.3 方法内联(Inlining):JIT 最关键的优化 graph TB
subgraph "内联前"
A1["caller"] -->|"call foo(x)"| A2["foo(x)<br/>栈帧、参数传递"]
end
subgraph "内联后"
B1["caller<br/>foo 代码直接展开<br/>参数变成局部变量"]
end
style A1 fill:#FFDAB9,stroke:#FFAB76,color:#333
style A2 fill:#E8D5F5,stroke:#CE93D8,color:#333
style B1 fill:#B5EAD7,stroke:#80CBC4,color:#333内联为什么有效 ?
优化 内联前 内联后 消除调用开销 每次调用 ~5ns 0 常量传播 看不到 foo 的常量 看到 foo 内的常量 死代码消除 无法跨函数 跨函数消除 逃逸分析 看不到对象用途 看清所有使用点 指令缓存 跳来跳去 连续代码
3.4 简单内联 Pass 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Inliner {public : bool should_inline (Function& caller, Function& callee, int depth) { return callee.code.size () < 20 && depth < 3 && caller.name != callee.name; } void inline_call (Function& caller, int call_site, Function& callee) { auto insert_pos = caller.code.begin () + call_site + 1 ; caller.code.insert (insert_pos, callee.code.begin (), callee.code.end ()); adjust_jumps (caller, call_site, callee.code.size ()); caller.code.erase (caller.code.begin () + call_site); } };
3.5 逃逸分析(Escape Analysis) 1 2 3 4 5 6 7 8 9 class Point { public : int x, y; };void foo () { Point p; p.x = 1 ; p.y = 2 ; use (p); }
逃逸状态 优化 NoEscape (不逃逸)栈分配 (不用堆)、标量替换 (拆成 2 个 int)ArgEscape (参数逃逸)同步消除 (无锁)GlobalEscape (全局逃逸)无优化
关键观察 :Java 的 new Point() 经过逃逸分析后,如果对象不逃逸,JIT 会把它拆成栈上的 2 个 int ——这相当于”消除 new”。
四、Tracing JIT:LuaJIT 的独门绝技 4.1 Method-based vs Tracing JIT 对比 维度 Method-based JIT Tracing JIT 编译单位 整个方法 热循环 / 热路径 分支处理 假设各 50% 只编译实际跑过的路径 代码体积 大(可能编译很多不走的分支) 小(只编译热路径) 适合 大量小方法调用 数值计算、循环密集 代表 HotSpot C2 LuaJIT、PyPy
4.2 热路径(Trace)记录 graph TB
START["循环入口"] --> H1["header guard<br/>(类型检查)"]
H1 --> B1["body line 1"]
B1 --> B2["body line 2"]
B2 --> B3["body line 3"]
B3 -->|"回边计数超阈值"| EXIT["side exit<br/>→ 解释器"]
EXIT -.->|"记录新 trace"| NEW["生成新的 trace"]
B3 -->|"回到 START"| START
style START fill:#FFB3C6,stroke:#F48FB1,color:#333
style H1 fill:#FFF9C4,stroke:#F9A825,color:#333
style B1 fill:#B5EAD7,stroke:#80CBC4,color:#333
style B2 fill:#B5EAD7,stroke:#80CBC4,color:#333
style B3 fill:#B5EAD7,stroke:#80CBC4,color:#333
style EXIT fill:#FFDAB9,stroke:#FFAB76,color:#333
style NEW fill:#E8D5F5,stroke:#CE93D8,color:#3334.3 Trace Tree 与 Guard graph TD
ROOT["ROOT trace<br/>fib(40) 主路径"] --> T1["Trace 1<br/>n >= 2<br/>(进入递归)"]
ROOT --> T2["Trace 2<br/>n < 2<br/>(side exit)"]
T1 -->|"head guard<br/>n 类型检查"| GUARD1["guard: n < 2 ?<br/>失败 → 退出"]
GUARD1 -->|"成功"| T1_BODY["Trace 1 body<br/>fib(n-1) + fib(n-2)"]
T2 --> T2_BODY["return n<br/>直接返回"]
style ROOT fill:#FFB3C6,stroke:#F48FB1,color:#333
style T1 fill:#E8D5F5,stroke:#CE93D8,color:#333
style T2 fill:#E8D5F5,stroke:#CE93D8,color:#333
style GUARD1 fill:#FFF9C4,stroke:#F9A825,color:#333
style T1_BODY fill:#B5EAD7,stroke:#80CBC4,color:#333
style T2_BODY fill:#B5EAD7,stroke:#80CBC4,color:#333核心思想 :每个 trace 是一条”直线路径” ——没有分支!分支点变成 guard (类型检查、值检查),检查失败就 side exit 回退到解释器。
4.4 Blackhole:防止无限记录 问题 :trace 记录时,guard 失败 → 退出 → 解释器 → 又进入循环 → 又记录……可能无限循环 。
解法 :trace 记录时,guard 失败的代码不真正执行 ——把它扔进 blackhole (黑洞):
1 2 3 4 5 6 7 8 9 10 11 12 13 void record_trace (Loop loop) { while (true ) { Instr i = next (loop); if (i is guard && guard_failed (i)) { log_side_exit (); break ; } append_to_trace (i); } }
4.5 手写 Mini Tracing JIT 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class MiniTracingJIT { std::vector<Trace> traces_; std::map<Bytecode*, int > loop_header_; public : int64_t execute (Program& prog) { Bytecode* pc = prog.entry (); while (pc) { if (auto * t = find_trace (pc)) { pc = (Bytecode*)run_trace (t); } else { pc = interpret_one (pc, prog); } if (pc && is_loop_header (pc) && backedge_count (pc) > THRESHOLD) { compile_trace (pc, prog); } } } void compile_trace (Bytecode* header, Program& prog) { Trace t; Bytecode* pc = header; std::set<Bytecode*> visited; while (true ) { Instr i = *pc; if (i.is_guard ()) { t.guards.push_back (i); pc = pc->successor (); continue ; } if (visited.count (pc)) break ; t.code.push_back (emit_native (i)); visited.insert (pc); pc = pc->next (); } traces_.push_back (t); loop_header_[header] = traces_.size () - 1 ; } };
关键技巧 :trace 是直线的、无分支的机器码 ——所有分支决策都变成了”运行时检查 + 跳转”。
五、隐藏类与内联缓存:V8 的核心武器 5.1 为什么动态语言需要”类”? JavaScript 没有静态类型——obj.x 可能是数字、字符串、对象……每次访问都要查类型。V8 的解法 :隐藏类(Hidden Class) ——给每个对象一个内部结构描述。
1 2 3 4 5 function Point (x, y ) { this .x = x; this .y = y; } var p = new Point (1 , 2 );
graph LR
P["p 对象"] --> HC["Hidden Class<br/>C0"]
HC -->|"x: offset 0<br/>y: offset 1"| L["布局:<br/>[x:int][y:int]"]
style P fill:#FFDAB9,stroke:#FFAB76,color:#333
style HC fill:#E8D5F5,stroke:#CE93D8,color:#333
style L fill:#B5EAD7,stroke:#80CBC4,color:#3335.2 Hidden Class 转换(Transition) graph LR
A["C0<br/>{}"] -->|"+ x = 1"| B["C1<br/>{x@0}"]
B -->|"+ y = 2"| C["C2<br/>{x@0, y@1}"]
C -->|"delete y"| D["C3<br/>{x@0}<br/>(退化)"]
D -->|"+ y = 3"| E["C4<br/>{x@0, y@1}<br/>(与 C2 共享)"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
style D fill:#FFDAB9,stroke:#FFAB76,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333关键观察 :相同字段顺序的对象共享 hidden class ——这让 V8 把 obj.x 编译成和静态语言一样的字段访问指令 。
5.3 内联缓存(Inline Cache, IC) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class InlineCache { HiddenClass* cached_class_ = nullptr ; void * cached_target_ = nullptr ; public : void * get_target (HiddenClass* cls) { if (cls == cached_class_) { return cached_target_; } return slow_path (cls); } };
5.4 内联缓存的 4 种状态 状态 类型数 性能 触发场景 未初始化 0 ❌ 极慢 首次调用 单态(Monomorphic) 1 ✅ 最快 同一类型多次调用 多态(Polymorphic) 2-4 ✅ 快 少量类型 超多态(Megamorphic) >4 ❌ 慢 大量类型(退化为 hashmap)
graph LR
A["🔵 单态<br/>1 个类型<br/>fast-path"] --> B["🟢 多态<br/>2-4 个类型<br/>switch"]
B --> C["🟡 超多态<br/>> 4 个类型<br/>hashmap"]
style A fill:#B5EAD7,stroke:#80CBC4,color:#333
style B fill:#FFF9C4,stroke:#F9A825,color:#333
style C fill:#FFB3C6,stroke:#F48FB1,color:#3335.5 手写 Mini Hidden Class 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 class HiddenClass {public : struct Field { std::string name; int offset; }; HiddenClass* transition (const std::string& field_name) { auto it = transitions_.find (field_name); if (it != transitions_.end ()) return it->second; HiddenClass* next = new HiddenClass (); next->fields_ = fields_; next->fields_.push_back ({field_name, (int )fields_.size ()}); transitions_[field_name] = next; return next; } int get_offset (const std::string& name) const { for (size_t i = 0 ; i < fields_.size (); ++i) { if (fields_[i].name == name) return fields_[i].offset; } return -1 ; } private : std::vector<Field> fields_; std::map<std::string, HiddenClass*> transitions_; }; class Object {public : Object (HiddenClass* cls, std::vector<int64_t > data) : class_ (cls), data_ (std::move (data)) {} int64_t get (const std::string& field) { int offset = class_->get_offset (field); return offset < 0 ? 0 : data_[offset]; } void set (const std::string& field, int64_t value) { int offset = class_->get_offset (field); if (offset < 0 ) { class_ = class_->transition (field); offset = class_->get_offset (field); data_.push_back (value); } else { data_[offset] = value; } } private : HiddenClass* class_; std::vector<int64_t > data_; };
5.6 Map 融合:TurboFan 的绝招 graph TB
P1["Point.x"] --> MAP["Map 1<br/>(Point, offset 0)"]
V1["Vec.x"] --> MAP2["Map 2<br/>(Vec, offset 0)"]
MAP --> FUSE["Map 融合<br/>offset 相同的属性"]
MAP2 --> FUSE
FUSE --> CODE["生成统一机器码<br/>data + 0"]
style P1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style V1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style MAP fill:#E8D5F5,stroke:#CE93D8,color:#333
style MAP2 fill:#E8D5F5,stroke:#CE93D8,color:#333
style FUSE fill:#FFF9C4,stroke:#F9A825,color:#333
style CODE fill:#B5EAD7,stroke:#80CBC4,color:#333核心思想 :只要 obj.x 的偏移量相同,不管 obj 是 Point 还是 Vec,都生成一样的 mov rax, [rdi + 0] ——这让 V8 在 polymorphic 状态下也能保持高性能。
六、反优化(Deoptimization):激进优化的”后悔药” 6.1 为什么需要 Deopt? flowchart TD
COMPILE["C2 激进编译<br/>假设 obj 一直是 Point"] --> EXECUTE["执行机器码"]
EXECUTE -->|"obj 实际是 Vec"| GUARD_FAIL["guard 失败<br/>(类型检查)"]
GUARD_FAIL --> DEOPT["Deopt<br/>回退到解释器"]
DEOPT --> RE_INTERP["重新解释执行"]
RE_INTERP -->|"重新 profile"| RE_COMPILE["重新编译<br/>(基于新 profile)"]
style COMPILE fill:#FFB3C6,stroke:#F48FB1,color:#333
style EXECUTE fill:#B5EAD7,stroke:#80CBC4,color:#333
style GUARD_FAIL fill:#FFDAB9,stroke:#FFAB76,color:#333
style DEOPT fill:#FFF9C4,stroke:#F9A825,color:#333
style RE_INTERP fill:#E8D5F5,stroke:#CE93D8,color:#333
style RE_COMPILE fill:#B5EAD7,stroke:#80CBC4,color:#3336.2 V8 的 5 大 Deopt 触发点 触发点 原因 例子 类型变化 假设 obj.x 是 int,实际变成 string IC miss 访问未初始化属性 假设 obj.x 已定义,实际 undefined hidden class transition 数组越界 假设 arr.length 不变,实际变化 数组变稀疏 多态上限 假设单态,实际第 5 种类型 megamorphic IC OSR 反优化 长期运行的循环 profile 变化 长循环跑久了
6.3 Safepoint 与 Stack Map Deopt 的工程难题 :回退到解释器时,所有栈上的值都要重新映射 ——解释器要的是”对象引用 + 原始值”,机器码要的是”寄存器 + 栈位置”。
graph TB
subgraph "JIT 机器码栈帧"
R1["rax = 0x1234<br/>(Point 引用)"]
R2["rbx = 42<br/>(int)"]
R3["栈[8] = 'hello'<br/>(string)"]
end
subgraph "Stack Map"
SM["rax → Point<br/>rbx → int<br/>栈[8] → string"]
end
R1 -.-> SM
R2 -.-> SM
R3 -.-> SM
SM --> DEOPT["Deopt 时<br/>按映射还原到解释器"]
style R1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style R2 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style R3 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style SM fill:#E8D5F5,stroke:#CE93D8,color:#333
style DEOPT fill:#FFB3C6,stroke:#F48FB1,color:#333概念 含义 Safepoint 程序中可以安全停止的位置(循环回边、方法入口) Stack Map 寄存器/栈槽 → 解释器变量的映射表 OSR 在循环回边替换栈帧(保留当前值)
6.4 Deopt 的 4 步流程 sequenceDiagram
participant JIT as JIT 机器码
participant Guard as Guard 检查
participant Runtime as Runtime
participant Interp as 解释器
JIT->>Guard: 执行 obj.x(假设 Point)
Guard->>Guard: 检查 obj.class == Point?
Guard->>Runtime: 不匹配,调用 deopt
Runtime->>Runtime: 1. 找到当前字节码 PC
Runtime->>Runtime: 2. 按 stack map 还原解释器状态
Runtime->>Interp: 3. 跳转到解释器入口
Interp->>Interp: 4. 重新解释执行 + 重新 profile七、HotSpot C1/C2 详细对比 7.1 编译触发时机 graph LR
A["🔵 Tier 0<br/>解释<br/>counter = 0"] -->|调用 1500| B["🟣 Tier 1<br/>C1 简单编译<br/>无 profile"]
B -->|调用 15000| C["🟢 Tier 2<br/>C1 + profile"]
C -->|调用 100000| D["🟠 Tier 3<br/>C2 激进优化"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
style D fill:#FFDAB9,stroke:#FFAB76,color:#3337.2 C1 vs C2 优化技术对比 优化 C1 C2 说明 方法内联 ✅ 浅 ✅ 深 C2 可内联 10+ 层 局部变量分析 ✅ ✅ 找不变表达式 全局值编号(GVN) ✅ ✅ 消除冗余计算 逃逸分析 ✅ 基础 ✅ 完整 C2 能做标量替换 循环展开 ❌ ✅ C2 展开 2-8 倍 向量化 ❌ ✅ SIMD 指令 分支预测优化 ✅ ✅ 激进 基于 profile 去优化支持 基础 完整 C2 必须支持完整 deopt 代码体积 小 大 C2 可能膨胀 3-5x 编译时间 ~1ms ~100ms C2 慢 100 倍
7.3 HotSpot 的 4 大 profile 计数器 graph TB
A["程序执行"] --> B["4 大 profile 计数器"]
B --> C1["🔵 调用计数<br/>InvocationCounter"]
B --> C2["🔵 回边计数<br/>BackedgeCounter"]
B --> C3["🔵 类型 profile<br/>TypeProfile"]
B --> C4["🔵 分支 profile<br/>BranchProfile"]
C3 --> C3D["记录 obj 的实际类型<br/>(Point 占 99%)"]
C4 --> C4D["记录 if 走向<br/>(then 占 95%)"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C2 fill:#FFF9C4,stroke:#F9A825,color:#333
style C3 fill:#FFF9C4,stroke:#F9A825,color:#333
style C4 fill:#FFF9C4,stroke:#F9A825,color:#333
style C3D fill:#B5EAD7,stroke:#80CBC4,color:#333
style C4D fill:#B5EAD7,stroke:#80CBC4,color:#333关键观察 :C2 的所有激进优化(类型特化、分支倒置)都基于 profile 数据 ——所以”profile 准确 = JIT 高效”。
八、性能对比:fib(40) 基准测试 8.1 实测数据 实现 编译时机 fib(40) 耗时 相对 C++ 性能 加速比 C++ AOT 运行前 ~5 ms 100% 1x V8 JIT (Node) 运行中 ~30 ms 17% 6x 慢 HotSpot C2 (Java) 运行中 ~25 ms 20% 5x 慢 LuaJIT 运行中 ~50 ms 10% 10x 慢 MiniJIT(本文) 运行中 ~300 ms 1.7% 60x 慢 AST 解释器(#1) 不编译 ~1500 ms 0.3% 300x 慢 CPython 3.10 不编译 ~3000 ms 0.17% 600x 慢
graph LR
A["🔴 CPython<br/>3000ms"] --> B["🟠 AST 解释<br/>1500ms"]
B --> C["🟡 字节码解释<br/>300ms"]
C --> D["🟢 MiniJIT<br/>300ms"]
D --> E["🔵 LuaJIT<br/>50ms"]
E --> F["🟣 V8 JIT<br/>30ms"]
F --> G["🟤 C++ AOT<br/>5ms"]
style A fill:#FFB3C6,stroke:#F48FB1,color:#333
style B fill:#FFDAB9,stroke:#FFAB76,color:#333
style C fill:#FFF9C4,stroke:#F9A825,color:#333
style D fill:#FFF9C4,stroke:#F9A825,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style F fill:#E8D5F5,stroke:#CE93D8,color:#333
style G fill:#C7CEEA,stroke:#9FA8DA,color:#3338.2 为什么 CPython 慢 600x? 因素 CPython 实际情况 优化空间 每次操作都查类型 PyObject_Add 查 type 表JIT 内联缓存 每个 int 都是对象 28 字节堆对象 + GC 压力 整数未装箱 无内联 fib(n-1)+fib(n-2) 2 层调用JIT 内联 无循环优化 不展开、不向量化 C2 激进优化 参考计数 每次 inc/dec 逃逸分析消除
关键观察 :600x 差距 = 类型检查(100x)+ 对象装箱(10x)+ 无内联(5x)+ 无优化(2x) ——JIT 能干掉前 3 个,但底层语言(C++)仍有 5x 优势。
8.3 Octane 基准测试简介 测试项 测什么 典型优化 Richards 操作系统调度模拟 内联、寄存器分配 DeltaBlue 约束求解 类型特化 Crypto RSA 算法 整数算术 RayTrace 3D 光线追踪 浮点向量化 EarleyBoyer 解析器 递归优化 Splay 树操作 数据结构特化
关键观察 :JIT 在 Crypto / RayTrace 这种”数值密集循环”上接近 AOT ——但在 Richards 这种”高度分支 + 大量调用”上仍有差距 。
九、实战:手写 200 行简单 JIT 9.1 整体设计 graph LR
SRC["第 1 篇 IR<br/>三地址码"] --> PARSE["解析 IR"]
PARSE --> COMPILE["SimpleJIT<br/>生成 x86-64"]
COMPILE --> BUF["code_buffer<br/>机器码"]
BUF --> MMAP["mmap<br/>PROT_EXEC"]
MMAP --> EXEC["execute()<br/>调用机器码"]
style SRC fill:#C7CEEA,stroke:#9FA8DA,color:#333
style PARSE fill:#E8D5F5,stroke:#CE93D8,color:#333
style COMPILE fill:#FFB3C6,stroke:#F48FB1,color:#333
style BUF fill:#FFF9C4,stroke:#F9A825,color:#333
style MMAP fill:#B5EAD7,stroke:#80CBC4,color:#333
style EXEC fill:#B5EAD7,stroke:#80CBC4,color:#3339.2 mini_jit.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #pragma once #include <vector> #include <cstdint> #include <map> #include <string> #include <stdexcept> #include <sys/mman.h> enum class IROp { PUSH, LOAD, STORE, ADD, SUB, MUL, LT, RET };struct IRInstr { IROp op; int64_t operand; }; class SimpleJIT {public : SimpleJIT () { code_buffer_.resize (4096 ); exec_mem_ = mmap (nullptr , 4096 , PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (exec_mem_ == MAP_FAILED) { throw std::runtime_error ("mmap failed" ); } } ~SimpleJIT () { munmap (exec_mem_, 4096 ); } void compile (const std::vector<IRInstr>& ir) ; int64_t execute (const std::map<std::string, int64_t >& args) ; private : std::vector<uint8_t > code_buffer_; void * exec_mem_ = nullptr ; size_t code_size_ = 0 ; int64_t locals_[16 ] = {0 }; };
9.3 x86-64 指令编码器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 class X64Emitter {public : std::vector<uint8_t >& buf; X64Emitter (std::vector<uint8_t >& b) : buf (b) {} void mov_rax_imm (int64_t imm) { emit (0x48 ); emit (0xB8 ); emit_q (imm); } void store_rax (int8_t offset) { emit (0x48 ); emit (0x89 ); emit (0x45 ); emit ((uint8_t )offset); } void load_rax (int8_t offset) { emit (0x48 ); emit (0x8B ); emit (0x45 ); emit ((uint8_t )offset); } void add_rax_rbx () { emit (0x48 ); emit (0x01 ); emit (0xD8 ); } void sub_rax_rbx () { emit (0x48 ); emit (0x29 ); emit (0xD8 ); } void imul_rax_rbx () { emit (0x48 ); emit (0x0F ); emit (0xAF ); emit (0xC3 ); } void cmp_rax_rbx () { emit (0x48 ); emit (0x39 ); emit (0xD8 ); } void setl_al () { emit (0x0F ); emit (0x9C ); emit (0xC0 ); } void ret () { emit (0xC3 ); } void prologue (int frame_size = 32 ) { emit (0x55 ); emit (0x48 ); emit (0x89 ); emit (0xE5 ); emit (0x48 ); emit (0x83 ); emit (0xEC ); emit ((uint8_t )frame_size); } void epilogue () { emit (0xC9 ); emit (0xC3 ); } private : void emit (uint8_t b) { buf.push_back (b); } void emit_q (int64_t v) { for (int i = 0 ; i < 8 ; ++i) emit ((uint8_t )(v >> (i * 8 ))); } };
9.4 SimpleJIT 编译入口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 void SimpleJIT::compile (const std::vector<IRInstr>& ir) { code_buffer_.clear (); X64Emitter emit (code_buffer_) ; emit.prologue (32 ); emit.mov_rax_imm (0 ); auto & first_store = code_buffer_; emit.store_rax (-8 ); for (auto & instr : ir) { switch (instr.op) { case IROp::PUSH: emit.mov_rax_imm (instr.operand); emit.store_rax (-16 ); break ; case IROp::LOAD: emit.load_rax (-8 - instr.operand * 8 ); emit.store_rax (-16 ); break ; case IROp::ADD: { emit.load_rax (-16 ); emit.mov_rbx_from (-8 - instr.operand * 8 ); emit.add_rax_rbx (); emit.store_rax (-16 ); break ; } } } emit.load_rax (-16 ); emit.epilogue (); std::memcpy (exec_mem_, code_buffer_.data (), code_buffer_.size ()); code_size_ = code_buffer_.size (); }
9.5 执行机器码 1 2 3 4 5 6 7 8 9 10 11 12 13 int64_t SimpleJIT::execute (const std::map<std::string, int64_t >& args) { int idx = 0 ; for (auto & [k, v] : args) { locals_[idx++] = v; } using JitFunc = int64_t (*)(int64_t *); JitFunc func = (JitFunc)exec_mem_; return func (locals_); }
9.6 性能测试:解释 vs JIT 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <chrono> int64_t fib_interpreted (int n) { if (n < 2 ) return n; return fib_interpreted (n - 1 ) + fib_interpreted (n - 2 ); } int64_t fib_jit (int n) { static SimpleJIT jit; static bool compiled = false ; if (!compiled) { std::vector<IRInstr> ir = { {IROp::LOAD, 0 }, {IROp::PUSH, 2 }, {IROp::LT, 0 }, {IROp::RET, 0 }, }; jit.compile (ir); compiled = true ; } return jit.execute ({{"n" , n}}); } void benchmark () { using namespace std::chrono; auto t1 = high_resolution_clock::now (); int64_t r1 = fib_interpreted (30 ); auto t2 = high_resolution_clock::now (); auto interp_time = duration_cast <milliseconds>(t2 - t1).count (); auto t3 = high_resolution_clock::now (); int64_t r2 = fib_jit (30 ); auto t4 = high_resolution_clock::now (); auto jit_time = duration_cast <milliseconds>(t4 - t3).count (); printf ("AST 解释器: %lld ms\n" , interp_time); printf ("Mini JIT: %lld ms\n" , jit_time); printf ("加速比: %.1fx\n" , (double )interp_time / jit_time); }
9.7 实测性能 1 2 3 4 5 6 $ ./benchmark AST 解释器: 1500 ms 字节码解释器: 300 ms Mini JIT: 280 ms ← 本文代码 V8 JIT: 30 ms C++ AOT: 5 ms
阶段 耗时 加速比 AST 解释 1500 ms 1x 字节码解释 300 ms 5x Mini JIT 280 ms 5.4x V8 JIT 30 ms 50x C++ AOT 5 ms 300x
关键观察 :本文 Mini JIT 跑出 5.4x 加速 ,证明了”运行时编译成机器码”的有效性。但距离 V8 还有 10x 差距——这就是 V8 的隐藏类 + IC + 完整优化的威力。
十、JIT 核心原理总结 10.1 JIT 编译的 8 个关键决策 决策 选项 代表选择 编译单位 方法 vs Trace HotSpot 方法 / LuaJIT Trace 触发时机 计数器 vs 采样 HotSpot 计数器(精确) 优化激进度 简单 vs 激进 V8 Sparkplug 简单 / TurboFan 激进 profile 收集 边解释边收集 vs 编译时收集 HotSpot 分层编译 类型特化 单态 vs 多态 vs 通用 V8 monomorphic 优先 内联策略 全部内联 vs 启发式 V8 启发式 去优化支持 支持 vs 不支持 V8 完整支持 OSR 支持 支持 vs 不支持 HotSpot 完整支持
10.2 三大 JIT 流派终极对比 流派 优势 劣势 适合场景 Method-based (HotSpot)通用、稳定、易实现 分支密度高时代码膨胀 通用业务逻辑 Tracing (LuaJIT)数值循环极快 控制流复杂时频繁 side exit 科学计算、游戏 Baseline + Optimizing (V8)启动快 + 峰值高 实现复杂 桌面应用、服务端
10.3 JIT 优化全景图 graph TB
A["字节码 / IR"] --> B["JIT 编译"]
B --> C["基础优化"]
C --> D["中级优化"]
D --> E["激进优化"]
E --> F["机器码"]
C --> C1["常量折叠<br/>死代码消除"]
D --> D1["方法内联<br/>逃逸分析"]
E --> E1["循环展开<br/>向量化<br/>类型特化"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#FFB3C6,stroke:#F48FB1,color:#333
style C fill:#E8D5F5,stroke:#CE93D8,color:#333
style D fill:#E8D5F5,stroke:#CE93D8,color:#333
style E fill:#E8D5F5,stroke:#CE93D8,color:#333
style F fill:#B5EAD7,stroke:#80CBC4,color:#333
style C1 fill:#B5EAD7,stroke:#80CBC4,color:#333
style D1 fill:#B5EAD7,stroke:#80CBC4,color:#333
style E1 fill:#B5EAD7,stroke:#80CBC4,color:#333十一、5 篇系列回顾 11.1 5 篇核心覆盖对比 维度 #1 前端 #2 优化 #3 后端 #4 LLVM #5 JIT 核心主题 词法/语法/语义/IR 8 大 Pass x86-64 代码生成 LLVM API JIT + 解释器 代码量 800 行 600 行 1000 行 500 行 600 行 覆盖范围 前端 4 阶段 中端优化 后端代码生成 工业工具链 运行时 实战项目 MiniLang 常量折叠 x86-64 codegen Kaleidoscope SimpleJIT 对比工具 ANTLR/PLY/Lark LLVM Pass GCC/binutils LLVM/MLIR HotSpot/V8 难度 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
11.2 从 #1 到 #5 的能力曲线 graph LR
A["#1<br/>源码 → IR"] --> B["#2<br/>IR 优化"] --> C["#3<br/>IR → 机器码"] --> D["#4<br/>LLVM 一站式"] --> E["#5<br/>运行时编译"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFF9C4,stroke:#F9A825,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333阶段 输入 输出 关键技术 #1 前端 源码字符串 三地址码 IR DFA + Pratt + SSA #2 优化 三地址码 IR 优化后 IR 常量折叠、死代码、循环优化 #3 后端 IR x86-64 机器码 指令选择 + 寄存器分配 #4 LLVM AST 优化机器码 LLVM API 一站式 #5 JIT 字节码 运行时机器码 解释器 + 编译触发 + 去优化
11.3 完整编译流程全貌 graph TB
SRC["📝 源码<br/>.c / .java / .js"] --> LEX["🔖 词法<br/>Token"]
LEX --> PARSE["🌳 语法<br/>AST"]
PARSE --> SEMA["🛡️ 语义<br/>类型检查"]
SEMA --> IR["⚙️ IR<br/>SSA / 字节码"]
IR --> OPT["✨ 优化<br/>几十个 Pass"]
OPT --> CODEGEN["💾 代码生成<br/>x86-64 / ARM"]
CODEGEN --> AOT["📦 AOT<br/>可执行文件"]
CODEGEN -.-> JIT["🚀 JIT<br/>运行时编译"]
IR -.-> INTERP["🐢 解释器<br/>直接执行"]
style SRC fill:#C7CEEA,stroke:#9FA8DA,color:#333
style LEX fill:#E8D5F5,stroke:#CE93D8,color:#333
style PARSE fill:#E8D5F5,stroke:#CE93D8,color:#333
style SEMA fill:#E8D5F5,stroke:#CE93D8,color:#333
style IR fill:#FFDAB9,stroke:#FFAB76,color:#333
style OPT fill:#FFF9C4,stroke:#F9A825,color:#333
style CODEGEN fill:#B5EAD7,stroke:#80CBC4,color:#333
style AOT fill:#B5EAD7,stroke:#80CBC4,color:#333
style JIT fill:#FFB3C6,stroke:#F48FB1,color:#333
style INTERP fill:#FFB3C6,stroke:#F48FB1,color:#333十二、继续学习路径 12.1 推荐书目 书名 重点 难度 《编译原理》(龙书) Alfred V. Aho编译原理圣经、所有概念的标准定义 ⭐⭐⭐⭐ 《现代编译原理》(虎书) Andrew W. Appel更现代、ML 项目贯穿 ⭐⭐⭐⭐ 《Engineering a Compiler》 Cooper & Torczon工程视角、比龙书易读 ⭐⭐⭐ 《虚拟机设计与实现》 Bill BlundenJIT、GC 细节深入 ⭐⭐⭐ 《Crafting Interpreters》 Robert NystromLox 解释器两本合一 ⭐⭐⭐ 《LuaJIT 文档》 Mike PallTracing JIT 实战参考 ⭐⭐⭐⭐⭐
12.2 3 个实战项目推荐 项目 难度 涉及技术 预计时间 写一个 Lua 子集解释器 + JIT ⭐⭐⭐⭐ 字节码、内联缓存 4 周 用 LLVM ORC 实现一个 REPL ⭐⭐⭐⭐ LLVM JIT API、TypeGenerator 2 周 实现 PyPy 风格的 Tracing JIT ⭐⭐⭐⭐⭐ 热路径记录、guard、side exit 8 周
12.3 推荐项目:Lua 子集解释器 + 简单 JIT graph LR
A["词法 + 语法"] --> B["AST"]
B --> C["字节码生成"]
C --> D["字节码解释器"]
C -.-> E["简单 JIT<br/>mmap + x86-64"]
D --> F["benchmark"]
E --> F
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFF9C4,stroke:#F9A825,color:#333
style E fill:#FFB3C6,stroke:#F48FB1,color:#333
style F fill:#B5EAD7,stroke:#80CBC4,color:#333实现步骤 :
步骤 内容 验证标准 1. 词法 Lua 关键字、字符串、数字 能识别 30+ token 2. 语法 表达式、语句、函数 能解析 fib 函数 3. 字节码 20 条指令 能解释执行 fib(10) 4. 内联缓存 单态 IC fib(20) 加速 5x 5. 简单 JIT mmap + x86-64 fib(20) 加速 20x
十三、常见误区与踩坑指南 13.1 JIT 5 大误区 误区 真相 “JIT 永远比 AOT 快” ❌ 启动期 JIT 反而慢;冷启动场景 AOT 必胜 “JIT 能追上 C++” ❌ 一般 5-10x 差距;类型检查开销难消除 “内联越多越好” ❌ 内联爆炸会触发 i-cache miss “deopt 是失败” ❌ deopt 是设计的一部分——激进优化的”后悔药” “profile 数据越多越好” ❌ profile 有开销;过时的 profile 反而有害
13.2 JIT 调试 5 大技巧 技巧 工具 看 JIT 编译日志 -XX:+PrintCompilation (HotSpot)看内联决策 -XX:+PrintInlining看 deopt 触发 -XX:+PrintDeoptimization看 OSR -XX:+PrintOSRV8 内部 node --trace-opt --trace-deopt
13.3 JIT 性能调优参数 参数 含义 调优建议 -XX:CompileThresholdC1 编译阈值 长跑服务调大(15000 → 30000) -XX:TieredStopAtLevel停在第几层 调试时用 =1 只用 C1 -XX:+UseCounterDecay计数器衰减 长跑服务开启 -XX:CICompilerCountJIT 线程数 CPU 密集型应用调小
十四、行动建议 14.1 三段行动建议 如果你刚学完本系列 :
从模仿开始 。把本文的 MiniJIT 200 行代码逐行重写一遍 ——编译自己的 fib、加几个新指令(MOD、AND),感受”运行时编译”和”编译时编译”的差异。
如果你想读 HotSpot/V8 源码 :
先读 CLion 的字节码解释器 ——src/hotspot/share/interpreter/bytecodeInterpreter.cpp 是 HotSpot 最易读的入口。理解 InterpreterGenerator、template table、safepoint,再去看 C1/C2 的 IR 优化。
如果你想实现一个生产 JIT :
从 LLVM ORC 开始 。ORC (On-Request Compilation) 是 LLVM 提供的”模块化 JIT API”——你不用写指令选择、寄存器分配,只写 IR 生成器,ORC 帮你编译并 mmap 执行。一个 Kaleidoscope JIT 只需 500 行 。
14.2 7 道递进思考题 # 难度 题目 1 ⭐ 为什么 AST 解释器比字节码解释器慢 5-10x? 2 ⭐⭐ 解释 OSR(On-Stack Replacement)为什么必要 3 ⭐⭐ Method-based vs Tracing JIT 各自的优缺点 4 ⭐⭐⭐ Hidden Class + IC 为什么能消除 90% 的类型检查? 5 ⭐⭐⭐ Deopt 的”栈映射”是怎么实现的? 6 ⭐⭐⭐⭐ 给本文 MiniJIT 加一个内联缓存(morphic IC) 7 ⭐⭐⭐⭐⭐ 把本文 MiniJIT 升级到 Tracing JIT(记录热循环)
14.3 终极思考:JIT 是未来吗? JIT 是 30 年来的工程奇迹 ——它让 Python/JavaScript/Lua 这些动态语言几乎 追上 C++ 性能。但 AOT 仍是性能下限,两者并非替代关系 :
WebAssembly + AOT :浏览器追求可预测的启动时间JIT 仍是动态语言首选 :类型信息只能运行时收集混合模式(Hybrid AOT+JIT) :Java AOT 编译 + JIT profile-guided 是未来趋势十五、本文小结 15.1 核心结论 JIT 编译 = 解释执行 + 运行时编译触发 + profile 引导的激进优化 + 去优化回退 。它的核心价值是把”何时编译”的决策权从程序员手中抢过来——交给运行时根据实际行为做出最优选择。
graph LR
A["🟡 字节码"] -->|解释执行| B["🐢 慢但快启动"]
B -->|热点检测| C["🚀 触发 JIT"]
C -->|profile 引导| D["✨ 激进优化"]
D -->|假设错误| E["🔴 Deopt"]
E -.->|重新编译| D
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#FFDAB9,stroke:#FFAB76,color:#333
style C fill:#FFF9C4,stroke:#F9A825,color:#333
style D fill:#B5EAD7,stroke:#80CBC4,color:#333
style E fill:#FFB3C6,stroke:#F48FB1,color:#33315.2 知识点回顾 知识点 状态 关键代码 JIT 触发条件 ✅ 掌握 counter + OSR 分层编译 ✅ 掌握 Tier 0 → Tier 4 方法内联 ✅ 掌握 Inliner 逃逸分析 ✅ 掌握 3 种逃逸状态 Tracing JIT ✅ 掌握 trace + guard Hidden Class ✅ 掌握 transition Inline Cache ✅ 掌握 单态/多态/超多态 OSR / Deopt ✅ 掌握 stack map 简单 JIT 实现 ✅ 掌握 200 行 mmap + x86-64 fib 性能对比 ✅ 掌握 5.4x 加速
15.3 5 篇系列最终回顾 graph LR
A["#1<br/>前端<br/>4 阶段"] --> B["#2<br/>优化<br/>8 Pass"]
B --> C["#3<br/>后端<br/>x86-64"]
C --> D["#4<br/>LLVM<br/>实战"]
D --> E["#5<br/>JIT<br/>运行时"]
E --> F["🎉 系列完结"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFF9C4,stroke:#F9A825,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style F fill:#FFB3C6,stroke:#F48FB1,stroke-width:4px,color:#333📚 编译原理实战 系列完结 · 全 5 篇 本文是《编译原理实战》系列第 5/5 篇,也是完结篇 。
📖 全部 5 篇目录(点击展开) 第 1 篇:手写 C++17 编译器前端——词法、语法、语义、IR 4 阶段全打通 第 2 篇:8 大优化 Pass 全打通——常量折叠、死代码消除、循环优化 第 3 篇:手写 x86-64 后端——指令选择、寄存器分配、机器码生成 第 4 篇:LLVM 实战——用 LLVM API 重写 mini 编译器 第 5 篇:JIT 编译实战——从解释器到 HotSpot/V8/LuaJIT ← 当前 · 系列完结🙏 写在最后 5 篇系列从源码的字符流出发,走过词法、语法、语义、IR、优化、后端、LLVM、JIT 全链路 ——约 4500 行 C++17 代码 ,覆盖了现代编译器的所有核心阶段 。
当你读完这 5 篇,你将获得什么?
能读懂 :GCC/Clang/LLVM/HotSpot/V8 的源码不再是黑盒能写出来 :从 0 实现一个 mini 编译器或 JIT能判断 :遇到性能问题时知道是 AOT 还是 JIT 的瓶颈能上手 :用 LLVM ORC 实现一个生产级 JIT 编译器编译原理不是”龙书里的抽象数学” ——它是 DFA + 递归下降 + 栈式符号表 + 三地址码 + SSA + 寄存器分配 + 内联缓存 这一系列具体工程模式的组合。一旦你亲手把这些模式各写一遍 ,100 万行 LLVM、80 万行 HotSpot、50 万行 V8 都只是这些模式的变体 ——再也不是黑盒。
系列完结,感谢阅读。
下一步 :选 3 个实战项目中的一个开始动手——写一个 Lua 子集解释器 + JIT 是最推荐的入门项目,预计 4 周可完成,能让你把本系列所有知识串联起来。
最后一句话 :JIT 是编译器的”运行时魔法” ——它让 Python/JavaScript/Lua 这样的动态语言几乎 追上 C++ 性能。但记住:600x 的性能差距不是”慢”——是物理规律 。每一条指令的执行时间、每一次 cache miss 的代价、每一个对象分配的 GC 压力,都真实存在。JIT 不能消除物理规律,但能巧妙地绕过它 ——这就是它的魅力。