JIT(Just-In-Time)编译是现代语言运行时的核心——它在程序运行时将热点代码编译为机器码,让解释型语言获得接近 AOT 编译的性能。Java 的 HotSpot、JavaScript 的 V8、Python 的 PyPy 都依赖 JIT 实现”解释器的灵活性 + 编译器的性能”。
JIT 编译是这一章的主角——LLVM 的 ORC API 如何支持 JIT?懒编译如何减少启动时间?内联缓存如何加速方法调用?
一、JIT 编译的核心思想
1.1 JIT 的工作原理
1.2 JIT vs AOT vs 解释执行
| 编译策略 | 启动速度 | 峰值性能 | 内存占用 | 典型实现 |
|---|---|---|---|---|
| 纯解释 | 最快 | 最慢 | 最低 | CPython, Ruby MRI |
| 纯 AOT | 慢(需全量编译) | 快 | 中 | Go, Rust, C++ |
| JIT(基线+优化) | 快 | 最快 | 最高 | V8, HotSpot JVM |
| JIT(全量编译) | 中 | 快 | 高 | Azul Zing, GraalVM |
JIT 的核心权衡是”编译开销 vs 执行收益”:热点代码编译一次、执行百万次,编译开销可以忽略;冷代码编译后只执行几次,编译开销反而拖慢启动。这就是为什么所有生产级 JIT 都采用分层编译。
| 特性 | AOT | 解释 | JIT |
|---|---|---|---|
| 编译时机 | 程序运行前 | 无编译 | 程序运行时 |
| 启动速度 | 慢 | 快 | 快 |
| 峰值性能 | 高 | 低 | 高 |
| 运行时信息 | 无 | 有 | 有 |
| 优化质量 | 好(全局) | 无 | 好(运行时数据) |
| 内存开销 | 低 | 低 | 高(代码缓存) |
| 典型代表 | C/Rust/Go | Python/Ruby | Java/JS/LuaJIT |
1.3 JIT 的核心优势
JIT 的核心优势是运行时信息——它可以看到 AOT 编译器看不到的信息:
| 运行时信息 | 优化机会 | 示例 |
|---|---|---|
| 实际类型 | 去虚化、内联 | obj.foo() → 直接调用 |
| 实际值 | 常量传播 | if (DEBUG) → 永远为假 |
| 调用频率 | 内联决策 | 热点函数内联 |
| 分支概率 | 布局优化 | 热路径放在一起 |
| 实际参数大小 | 边界检查消除 | 数组访问已知范围 |
二、LLVM ORC JIT API
2.1 ORC API 的设计
ORC(On-Request Compilation)是 LLVM 的现代 JIT API,支持懒编译、并发编译和远程 JIT:
2.2 使用 ORC JIT
// LLVM ORC JIT 最小示例#include "llvm/ExecutionEngine/Orc/LLJIT.h"#include "llvm/IR/IRBuilder.h"#include "llvm/IR/LLVMContext.h"#include "llvm/IR/Module.h"
using namespace llvm;using namespace llvm::orc;
int main() { // 1. 创建 JIT 实例 auto JIT = LLJITBuilder().create(); if (!JIT) { errs() << "Failed to create JIT: " << toString(JIT.takeError()) << "\n"; return 1; }
// 2. 创建 IR 模块 LLVMContext Ctx; auto M = std::make_unique<Module>("jit_module", Ctx); M->setDataLayout((*JIT)->getDataLayout());
// 3. 定义函数 FunctionType *FT = FunctionType::get( Type::getInt32Ty(Ctx), {Type::getInt32Ty(Ctx)}, false); Function *F = Function::Create(FT, Function::ExternalLinkage, "add_one", M.get());
// 4. 生成 IR BasicBlock *BB = BasicBlock::Create(Ctx, "entry", F); IRBuilder<> Builder(BB); Value *Arg = &*F->arg_begin(); Value *One = ConstantInt::get(Type::getInt32Ty(Ctx), 1); Value *Result = Builder.CreateAdd(Arg, One, "result"); Builder.CreateRet(Result);
// 5. 添加模块到 JIT auto Err = (*JIT)->addIRModule(ThreadSafeModule(std::move(M), std::move(Ctx))); if (Err) { errs() << "Failed to add module: " << toString(std::move(Err)) << "\n"; return 1; }
// 6. 查找并执行函数 auto Sym = (*JIT)->lookup("add_one"); if (!Sym) { errs() << "Function not found\n"; return 1; }
auto *Fn = Sym->toPtr<int(int)>(); int result = Fn(41); outs() << "add_one(41) = " << result << "\n"; // 输出 42
return 0;}2.3 ORC JIT 的层次
| 层 | 功能 | 说明 |
|---|---|---|
| ObjectLinkingLayer | 链接目标文件 | 最底层,使用 JITLink |
| IRCompileLayer | IR → 机器码 | 调用 LLVM 编译管线 |
| IRTransformLayer | IR 变换 | 优化 Pass |
| CompileOnDemandLayer | 懒编译 | 按需编译函数 |
| IndirectionUtils | 间接调用 | 支持运行时替换 |
三、懒编译(Lazy Compilation)
3.1 懒编译的原理
懒编译只在函数第一次被调用时才编译——避免编译永远不会执行的代码:
3.2 编译桩(Compilation Stub)
# 编译桩的伪代码def make_stub(func_name): """为未编译的函数创建编译桩""" def stub(*args): # 1. 触发编译 compiled_code = jit_compile(func_name)
# 2. 更新函数指针(后续调用直接跳到编译后的代码) update_function_pointer(func_name, compiled_code)
# 3. 执行编译后的代码 return compiled_code(*args)
return stub3.3 懒编译 vs 急编译
| 特性 | 懒编译 | 急编译 |
|---|---|---|
| 启动时间 | 快 | 慢 |
| 内存使用 | 低(只编译用到的) | 高(编译所有) |
| 峰值性能 | 有延迟(首次调用编译) | 无延迟 |
| 实现复杂度 | 高 | 低 |
| 适用场景 | 大型应用 | 小型程序 |
四、内联缓存(Inline Cache)
4.1 内联缓存的原理
内联缓存是 JIT 的核心优化——它缓存方法调用的目标地址,避免每次调用都进行方法查找:
# 无内联缓存:每次调用都查找方法def call_method(obj, method_name, *args): method = obj.__class__.lookup(method_name) # O(n) 查找 return method(obj, *args)
# 有内联缓存:缓存上次查找结果class InlineCache: def __init__(self): self.cached_class = None self.cached_method = None
def call(self, obj, method_name, *args): obj_class = obj.__class__
if obj_class == self.cached_class: # 缓存命中:直接调用 return self.cached_method(obj, *args) else: # 缓存未命中:查找并更新缓存 method = obj_class.lookup(method_name) self.cached_class = obj_class self.cached_method = method return method(obj, *args)4.2 多态内联缓存
单态内联缓存只缓存一个类。多态内联缓存缓存多个类:
| 缓存类型 | 缓存数量 | 命中率 | 适用场景 |
|---|---|---|---|
| 单态 | 1 | 高(单态调用点) | 大多数调用 |
| 多态 | 2-4 | 中 | 少量多态 |
| 超多态 | >4 | 低 | 需要虚表查找 |
4.3 内联缓存与去虚化
内联缓存使去虚化成为可能——将虚调用替换为直接调用:
// Java 虚调用animal.speak(); // 可能是 Dog.speak() 或 Cat.speak()
// 内联缓存发现 95% 的时候是 Dog// 去虚化 + 守卫条件:if (animal instanceof Dog) { Dog.speak(animal); // 直接调用,可内联} else { animal.speak(); // 回退到虚调用}五、Speculation 与去优化
5.1 Speculation(投机优化)
JIT 可以基于运行时观察到的信息做投机优化——假设某种条件总是成立,并插入守卫条件:
5.2 去优化(Deoptimization)
当投机优化的假设不成立时,JIT 需要去优化——从优化后的机器码回到解释器:
class DeoptimizationManager: """去优化管理器"""
def __init__(self): self.guard_count = 0 self.deopt_count = 0
def insert_guard(self, condition, fallback_interp_state): """插入守卫条件""" self.guard_count += 1 guard_id = self.guard_count
def guarded_execute(*args): if condition(*args): # 守卫通过:执行优化路径 return optimized_code(*args) else: # 守卫失败:去优化 self.deopt_count += 1 return self.deoptimize(guard_id, fallback_interp_state, args)
return guarded_execute
def deoptimize(self, guard_id, interp_state, args): """去优化:回到解释器""" # 1. 恢复解释器状态 # 2. 从正确的位置继续执行 # 3. 可能重新编译(不包含失败的假设) return interpret(interp_state, args)5.3 去优化的代价
| 代价 | 说明 |
|---|---|
| 编译时间 | 需要保存元数据用于恢复 |
| 内存 | 需要保存解释器状态映射 |
| 运行时 | 守卫条件有额外开销 |
| 去优化本身 | 可能很慢(需要重建栈帧) |
过多的去优化会导致抖动(Thrashing)——JIT 编译 → 去优化 → 重新编译 → 再次去优化……这比纯解释执行还慢。JIT 需要合理的投机策略来避免抖动。
六、分层编译
6.1 分层编译的原理
分层编译使用不同优化级别编译同一代码——先用快速编译(低优化)启动,再用慢速编译(高优化)替换热点代码:
6.2 分层编译的实现
| 层级 | 编译速度 | 优化质量 | 代码质量 | 适用 |
|---|---|---|---|---|
| Tier 0 | 最快 | 无 | 最低 | 冷代码 |
| Tier 1 | 快 | 基本 | 中等 | 温代码 |
| Tier 2 | 慢 | 激进 | 最高 | 热代码 |
6.3 不同 JIT 的分层策略
| JIT | 分层数 | 策略 |
|---|---|---|
| HotSpot (Java) | 3 | C1(快速) → C2(完整) |
| V8 (JavaScript) | 2 | Ignition → TurboFan |
| PyPy (Python) | 2 | 解释 → Tracing JIT |
| LuaJIT | 2 | 解释 → 记录→编译 |
| .NET CLR | 2 | Quick JIT → Optimizing JIT |
六-B、ORC JIT 深入与动态代码生成
6B.1 ORC JIT 的核心组件
ORC JIT 的设计围绕可组合的层——每一层添加一个能力,你可以按需组合:
| 组件 | 职责 | 可替换性 |
|---|---|---|
| ExecutionSession | 管理 JIT 生命周期和错误 | 核心组件 |
| JITDylib | 符号表,类似动态库 | 可创建多个 |
| CompileOnDemandLayer | 懒编译调度 | 可替换为急编译 |
| IRTransformLayer | IR 优化 Pass | 可自定义优化管线 |
| IRCompileLayer | IR → 目标码编译 | 可替换为自定义编译器 |
| ObjectLinkingLayer | 链接和内存分配 | 可替换为 JITLink |
6B.2 动态代码生成实践
JIT 不仅仅用于”编译已有代码”——它还可以在运行时生成全新的代码:
// 运行时生成一个 N 次多项式求值函数// f(x) = c0 + c1*x + c2*x^2 + ... + cn*x^nstd::unique_ptr<Module> buildPolynomial(LLVMContext &Ctx, const std::vector<double> &coeffs) { auto M = std::make_unique<Module>("poly", Ctx);
// 声明函数: double poly(double x) FunctionType *FT = FunctionType::get( Type::getDoubleTy(Ctx), {Type::getDoubleTy(Ctx)}, false); Function *F = Function::Create(FT, Function::ExternalLinkage, "poly", M.get());
BasicBlock *BB = BasicBlock::Create(Ctx, "entry", F); IRBuilder<> Builder(BB);
Value *X = &*F->arg_begin(); Value *Result = ConstantFP::get(Ctx, APFloat(coeffs[0]));
// Horner 法则: c0 + x*(c1 + x*(c2 + ...)) for (size_t i = 1; i < coeffs.size(); i++) { Value *C = ConstantFP::get(Ctx, APFloat(coeffs[i])); Value *Sum = Builder.CreateFAdd(C, Result, "sum"); Result = Builder.CreateFMul(X, Sum, "prod"); }
Builder.CreateRet(Result); return M;}6B.3 JIT vs AOT 全面对比
| 维度 | AOT 编译 | JIT 编译 |
|---|---|---|
| 编译时机 | 构建时 | 运行时 |
| 优化信息 | 静态分析 | 运行时 profiling |
| 去虚化 | 保守(CHA/RTA) | 激进(类型反馈) |
| 内联决策 | 基于启发式 | 基于调用频率+类型 |
| 常量折叠 | 编译期常量 | 运行期常量(更强) |
| 启动延迟 | 无(已编译) | 有(运行时编译) |
| 内存占用 | 低 | 高(代码缓存+元数据) |
| 调试体验 | 好 | 差(优化后代码难调试) |
| 可移植性 | 一份二进制 | 源码/字节码可跨平台 |
| 安全性 | 可审计 | 代码注入风险 |
JIT 编译的代码缓存是常见的攻击面——攻击者可能注入恶意代码到 JIT 缓存中。现代 JIT(如 V8)使用 W^X(Write XOR Execute)保护:代码区域要么可写要么可执行,不能同时两者。这通过切换内存页权限实现:写入时 RW,执行时 RX。
6B.4 JIT 编译的实际应用
| 应用 | JIT 实现 | 语言 | 场景 |
|---|---|---|---|
| HotSpot JVM | C1/C2 分层 | Java | 服务端应用 |
| V8 | Ignition+TurboFan | JavaScript | 浏览器/Node.js |
| PyPy | Tracing JIT | Python | 科学计算 |
| LuaJIT | 记录+编译 | Lua | 游戏脚本 |
| .NET CLR | RyuJIT | C#/F# | 桌面/服务端 |
| Julia | LLVM ORC | Julia | 科学计算 |
| GraalVM | Truffle+Graal | 多语言 | 跨语言互操作 |
七、动手实践
7.1 实验一:使用 LLVM ORC JIT
# 编译 ORC JIT 示例clang++ -g orc_jit.cpp $(llvm-config --cxxflags --ldflags --libs core orcjit native) -o orc_jit./orc_jit7.2 实验二:观察 Java JIT
# 启用 JIT 日志java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining MyApp
# 查看分层编译java -XX:+PrintCompilation -XX:TieredStopAtLevel=1 MyApp # 只用 C1java -XX:+PrintCompilation -XX:-TieredCompilation MyApp # 只用 C27.3 实验三:观察 V8 JIT
# 启用 V8 JIT 日志node --trace-opt --trace-deopt script.js
# 查看优化/去优化node --prof script.jsnode --prof-process isolate-*.log八、本章小结
在上一章中,LLVM 的 Pass 框架和模块化架构展示了 AOT 编译器的设计哲学——编译在程序运行前一次性完成。但有些语言的选择不同:它们以解释方式启动,在运行时将热点代码编译为机器码。这种”边跑边编译”的策略就是 JIT 编译,它让解释型语言获得了接近 AOT 的性能。
| 概念 | 要点 |
|---|---|
| JIT 核心 | 运行时编译热点代码,利用运行时信息优化 |
| ORC API | LLVM 的现代 JIT API,支持懒编译和并发 |
| 懒编译 | 只在函数首次调用时编译,减少启动时间 |
| 内联缓存 | 缓存方法调用目标,避免重复查找 |
| 去虚化 | 将虚调用替换为直接调用+守卫 |
| Speculation | 基于运行时观察做投机优化 |
| 去优化 | 投机失败时回到解释器 |
| 分层编译 | 不同优化级别编译同一代码 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






