mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2185 字
6 分钟
JIT 编译:运行时代码生成
2026-02-09

JIT(Just-In-Time)编译是现代语言运行时的核心——它在程序运行时将热点代码编译为机器码,让解释型语言获得接近 AOT 编译的性能。Java 的 HotSpot、JavaScript 的 V8、Python 的 PyPy 都依赖 JIT 实现”解释器的灵活性 + 编译器的性能”。

JIT 编译是这一章的主角——LLVM 的 ORC API 如何支持 JIT?懒编译如何减少启动时间?内联缓存如何加速方法调用?

一、JIT 编译的核心思想#

1.1 JIT 的工作原理#

flowchart TB SRC["源码/字节码"] --> INTERP["解释执行"] INTERP --> COUNT["执行计数"] COUNT --> HOT{热点?} HOT |否| INTERP HOT |是| OPT2["JIT 编译"] OPT2 --> CACHE["机器码缓存"] CACHE --> EXEC["直接执行机器码"] EXEC --> COUNT style SRC fill:#e3f2fd,stroke:#1565c0 style INTERP fill:#fff3e0,stroke:#e65100 style CACHE fill:#e8f5e9,stroke:#2e7d32 style EXEC fill:#fce4ec,stroke:#c62828

1.2 JIT vs AOT vs 解释执行#

编译策略启动速度峰值性能内存占用典型实现
纯解释最快最慢最低CPython, Ruby MRI
纯 AOT慢(需全量编译)Go, Rust, C++
JIT(基线+优化)最快最高V8, HotSpot JVM
JIT(全量编译)Azul Zing, GraalVM
Note

JIT 的核心权衡是”编译开销 vs 执行收益”:热点代码编译一次、执行百万次,编译开销可以忽略;冷代码编译后只执行几次,编译开销反而拖慢启动。这就是为什么所有生产级 JIT 都采用分层编译。

特性AOT解释JIT
编译时机程序运行前无编译程序运行时
启动速度
峰值性能
运行时信息
优化质量好(全局)好(运行时数据)
内存开销高(代码缓存)
典型代表C/Rust/GoPython/RubyJava/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:

flowchart TB subgraph ORC_JIT["ORC JIT 架构"] SESSION["ExecutionSession<br/>JIT 会话"] JD["JITDylib<br/>符号表"] ES["ExecutionSession"] CM["CompileOnDemandLayer<br/>懒编译"] IR["IRCompileLayer<br/>IR→机器码"] OBJ["ObjectLinkingLayer<br/>链接目标文件"] end SESSION --> JD --> CM --> IR --> OBJ style ORC_JIT fill:#e3f2fd,stroke:#1565c0

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
IRCompileLayerIR → 机器码调用 LLVM 编译管线
IRTransformLayerIR 变换优化 Pass
CompileOnDemandLayer懒编译按需编译函数
IndirectionUtils间接调用支持运行时替换

三、懒编译(Lazy Compilation)#

3.1 懒编译的原理#

懒编译只在函数第一次被调用时才编译——避免编译永远不会执行的代码:

flowchart TB CALL["调用函数 foo()"] --> STUB["跳转到编译桩"] STUB --> COMPILED{已编译?} COMPILED |否| COMPILE["JIT 编译 foo()"] COMPILE --> UPDATE["更新跳转表"] UPDATE --> EXEC["执行 foo()"] COMPILED |是| EXEC style CALL fill:#e3f2fd,stroke:#1565c0 style COMPILE fill:#fff3e0,stroke:#e65100 style EXEC fill:#e8f5e9,stroke:#2e7d32

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 stub

3.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 可以基于运行时观察到的信息做投机优化——假设某种条件总是成立,并插入守卫条件:

flowchart TB OBSERVE["观察运行时行为"] --> ASSUME["做出假设"] ASSUME --> OPT["基于假设优化"] OPT --> GUARD["插入守卫条件"] GUARD --> EXEC["执行优化代码"] EXEC --> CHECK{守卫通过?} CHECK |是| FAST["快速路径"] CHECK |否| DEOPT["去优化<br/>回到解释器"] style OBSERVE fill:#e3f2fd,stroke:#1565c0 style OPT fill:#e8f5e9,stroke:#2e7d32 style DEOPT fill:#fce4ec,stroke:#c62828

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 去优化的代价#

代价说明
编译时间需要保存元数据用于恢复
内存需要保存解释器状态映射
运行时守卫条件有额外开销
去优化本身可能很慢(需要重建栈帧)
Warning

过多的去优化会导致抖动(Thrashing)——JIT 编译 → 去优化 → 重新编译 → 再次去优化……这比纯解释执行还慢。JIT 需要合理的投机策略来避免抖动。

六、分层编译#

6.1 分层编译的原理#

分层编译使用不同优化级别编译同一代码——先用快速编译(低优化)启动,再用慢速编译(高优化)替换热点代码:

flowchart TB CODE["方法代码"] --> T0["Tier 0<br/>解释执行"] T0 -->|热点| T1["Tier 1<br/>快速编译<br/>简单优化"] T1 -->|极热| T2["Tier 2<br/>完整编译<br/>激进优化"] T2 -->|去优化| T0 style T0 fill:#e3f2fd,stroke:#1565c0 style T1 fill:#fff3e0,stroke:#e65100 style T2 fill:#e8f5e9,stroke:#2e7d32

6.2 分层编译的实现#

层级编译速度优化质量代码质量适用
Tier 0最快最低冷代码
Tier 1基本中等温代码
Tier 2激进最高热代码

6.3 不同 JIT 的分层策略#

JIT分层数策略
HotSpot (Java)3C1(快速) → C2(完整)
V8 (JavaScript)2Ignition → TurboFan
PyPy (Python)2解释 → Tracing JIT
LuaJIT2解释 → 记录→编译
.NET CLR2Quick JIT → Optimizing JIT

六-B、ORC JIT 深入与动态代码生成#

6B.1 ORC JIT 的核心组件#

ORC JIT 的设计围绕可组合的层——每一层添加一个能力,你可以按需组合:

flowchart TB TOP["用户代码"] --> COD["CompileOnDemandLayer<br/>懒编译:按需触发"] COD --> IRT["IRTransformLayer<br/>IR 优化:opt pipeline"] IRT --> IRC["IRCompileLayer<br/>IR → 目标文件"] IRC --> OLK["ObjectLinkingLayer<br/>目标文件 → 可执行内存"] OLK --> MEM["InProcessMemory<br/>可执行内存分配"] style COD fill:#e3f2fd,stroke:#1565c0 style IRC fill:#e8f5e9,stroke:#2e7d32 style OLK fill:#fff3e0,stroke:#e65100
组件职责可替换性
ExecutionSession管理 JIT 生命周期和错误核心组件
JITDylib符号表,类似动态库可创建多个
CompileOnDemandLayer懒编译调度可替换为急编译
IRTransformLayerIR 优化 Pass可自定义优化管线
IRCompileLayerIR → 目标码编译可替换为自定义编译器
ObjectLinkingLayer链接和内存分配可替换为 JITLink

6B.2 动态代码生成实践#

JIT 不仅仅用于”编译已有代码”——它还可以在运行时生成全新的代码

// 运行时生成一个 N 次多项式求值函数
// f(x) = c0 + c1*x + c2*x^2 + ... + cn*x^n
std::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)激进(类型反馈)
内联决策基于启发式基于调用频率+类型
常量折叠编译期常量运行期常量(更强)
启动延迟无(已编译)有(运行时编译)
内存占用高(代码缓存+元数据)
调试体验差(优化后代码难调试)
可移植性一份二进制源码/字节码可跨平台
安全性可审计代码注入风险
Warning

JIT 编译的代码缓存是常见的攻击面——攻击者可能注入恶意代码到 JIT 缓存中。现代 JIT(如 V8)使用 W^X(Write XOR Execute)保护:代码区域要么可写要么可执行,不能同时两者。这通过切换内存页权限实现:写入时 RW,执行时 RX。

6B.4 JIT 编译的实际应用#

应用JIT 实现语言场景
HotSpot JVMC1/C2 分层Java服务端应用
V8Ignition+TurboFanJavaScript浏览器/Node.js
PyPyTracing JITPython科学计算
LuaJIT记录+编译Lua游戏脚本
.NET CLRRyuJITC#/F#桌面/服务端
JuliaLLVM ORCJulia科学计算
GraalVMTruffle+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_jit

7.2 实验二:观察 Java JIT#

# 启用 JIT 日志
java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining MyApp
# 查看分层编译
java -XX:+PrintCompilation -XX:TieredStopAtLevel=1 MyApp # 只用 C1
java -XX:+PrintCompilation -XX:-TieredCompilation MyApp # 只用 C2

7.3 实验三:观察 V8 JIT#

# 启用 V8 JIT 日志
node --trace-opt --trace-deopt script.js
# 查看优化/去优化
node --prof script.js
node --prof-process isolate-*.log

八、本章小结#

上一章中,LLVM 的 Pass 框架和模块化架构展示了 AOT 编译器的设计哲学——编译在程序运行前一次性完成。但有些语言的选择不同:它们以解释方式启动,在运行时将热点代码编译为机器码。这种”边跑边编译”的策略就是 JIT 编译,它让解释型语言获得了接近 AOT 的性能。

概念要点
JIT 核心运行时编译热点代码,利用运行时信息优化
ORC APILLVM 的现代 JIT API,支持懒编译和并发
懒编译只在函数首次调用时编译,减少启动时间
内联缓存缓存方法调用目标,避免重复查找
去虚化将虚调用替换为直接调用+守卫
Speculation基于运行时观察做投机优化
去优化投机失败时回到解释器
分层编译不同优化级别编译同一代码

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

JIT 编译:运行时代码生成
https://blog.souloss.com/posts/compiler/jit-compilation/
作者
Souloss
发布于
2026-02-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时