本章深入探讨 LLVM 在即时编译(JIT)领域的应用与演进。我们将追溯从早期的传统 JIT 到现代 ORC JIT 架构的发展历程,分析 LLVM 如何成为构建高性能 JIT 编译器的首选基础设施。通过研究 Julia、PostgreSQL 等项目的集成经验,理解 JIT 编译在现代软件栈中的关键作用。
即时编译(Just-In-Time Compilation)代表了一种介于解释执行和静态编译之间的执行策略。与传统的提前编译(Ahead-of-Time, AOT)不同,JIT 编译器在程序运行时将中间表示或源代码编译为机器码。
在 LLVM 出现之前,构建 JIT 编译器需要从零开始实现整个编译管线。Java HotSpot、.NET CLR 等系统都包含了自己的 JIT 实现,这些实现往往与特定语言或运行时紧密耦合。LLVM 的模块化设计改变了这一局面:
传统 JIT 架构:
源代码 → 字节码 → [专用 JIT 编译器] → 机器码
LLVM JIT 架构:
源代码 → LLVM IR → [LLVM JIT 基础设施] → 机器码
↑
通用优化管线
多后端支持
调试信息生成
LLVM 为 JIT 编译提供了独特的价值主张:
成熟的优化管线复用:JIT 编译器可以直接使用 LLVM 的数百个优化 Pass,无需重新实现。这包括内联、循环优化、向量化等关键优化。
多目标架构支持:一次实现,多平台运行。LLVM 的后端支持让 JIT 编译器自动获得对 x86、ARM、RISC-V 等架构的支持。
增量式编译能力:LLVM 的模块化设计支持函数级别的编译和链接,非常适合 JIT 场景的按需编译。
Profile-Guided Optimization (PGO):运行时收集的性能数据可以直接反馈给 LLVM 优化器,实现自适应优化。
LLVM 的 JIT 支持经历了三个主要阶段:
第一代:Legacy JIT(2003-2009)
第二代:MCJIT(2010-2016)
第三代:ORC JIT(2015-至今)
早期的 LLVM JIT 实现虽然开创性,但存在严重的架构限制:
单一地址空间假设:Legacy JIT 假设编译和执行在同一进程中,无法支持跨进程或远程 JIT。
不完整的运行时支持:缺乏对异常处理、线程本地存储(TLS)、调试信息的完整支持。
内存管理问题:代码和数据的内存布局不灵活,难以实现代码缓存和垃圾回收。
符号解析限制:符号查找机制过于简单,不支持复杂的链接语义。
MCJIT(Machine Code JIT)在 2010 年被引入,旨在解决 Legacy JIT 的问题:
// MCJIT 的基本使用模式
std::unique_ptr<Module> M = parseModule(IRString);
ExecutionEngine *EE = EngineBuilder(std::move(M))
.setEngineKind(EngineKind::JIT)
.setUseMCJIT(true)
.create();
// 编译整个模块
EE->finalizeObject();
// 查找并执行函数
auto MainFn = EE->getFunctionAddress("main");
typedef int (*MainFnTy)();
int Result = reinterpret_cast<MainFnTy>(MainFn)();
MCJIT 的主要改进:
然而,MCJIT 仍有重要限制:
ORC(On-Request Compilation)JIT 是 Lang Hames 在 2015 年开始设计的全新架构,从根本上重新思考了 JIT 编译器的设计:
ORC 引入了 JITDylib(JIT Dynamic Library)概念,模拟动态链接库的行为:
// ORC JIT 的模块化设计
class JITDylib {
// 符号表:名称 → 地址映射
SymbolTable Symbols;
// 材料化单元:延迟编译的基本单位
std::vector<std::unique_ptr<MaterializationUnit>> MUs;
// 依赖关系追踪
DependencyGraph Dependencies;
};
ORC 采用分层设计,每层负责特定功能:
应用层
↓
LLJIT(高级 API)
↓
ExecutionSession(会话管理)
↓
IRCompileLayer(IR 编译)
↓
IRTransformLayer(IR 转换)
↓
ObjectLayer(对象文件处理)
↓
RTDyldObjectLinkingLayer(链接)
↓
机器码执行
ORC 的延迟编译通过 MaterializationUnit 实现:
class MaterializationUnit {
public:
// 声明此单元提供的符号
virtual SymbolFlagsMap getSymbols() = 0;
// 当符号被请求时触发编译
virtual void materialize(MaterializationResponsibility R) = 0;
// 支持丢弃未使用的定义
virtual void discard(const JITDylib &JD, SymbolStringPtr Name) {}
};
ORC 原生支持多线程编译,通过 ThreadSafeModule 确保线程安全:
ThreadSafeModule TSM(std::move(M), std::make_unique<LLVMContext>());
// 可以在多个线程中并发编译不同模块
CompileThreadPool.async([TSM = std::move(TSM)]() mutable {
return compileModule(std::move(TSM));
});
ORC 支持跨进程和跨机器的 JIT 编译:
主机进程 目标进程
↓ ↑
编译 LLVM IR 执行机器码
↓ ↑
通过 RPC 发送对象文件 → 加载并链接
ORC 支持投机编译,预测性地编译可能执行的代码:
class SpeculativeJIT {
// 基于运行时profile预测热点函数
void speculativelyCompile(Function *F) {
if (isProbablyHot(F)) {
BackgroundQueue.push(F);
CompileThread.notify();
}
}
};
JIT 编译的启动开销是其主要挑战之一。编译所有代码会导致严重的启动延迟,而延迟编译(Lazy Compilation)通过按需编译来缓解这个问题。
延迟编译的核心思想:
ORC 通过 LazyCallThroughManager 实现延迟编译:
// 延迟编译的工作流程
class LazyCallThroughManager {
// 1. 为每个延迟函数创建桩
JITTargetAddress createStub(Symbol S) {
// 分配可执行内存
auto StubAddr = allocateStub();
// 生成跳转到 resolver 的指令
emitResolverCall(StubAddr, S);
return StubAddr;
}
// 2. 解析时触发编译
JITTargetAddress resolve(Symbol S) {
// 查找符号对应的材料化单元
auto MU = findMaterializationUnit(S);
// 触发编译
MU->materialize();
// 返回编译后的地址
return getCompiledAddress(S);
}
};
桩函数的汇编实现(x86-64):
lazy_stub_for_foo:
; 保存寄存器状态
push %rax
push %rcx
push %rdx
push %rsi
push %rdi
push %r8
push %r9
; 调用解析器
movq $foo_symbol_id, %rdi
call lazy_resolver
; 恢复寄存器
pop %r9
pop %r8
pop %rdi
pop %rsi
pop %rdx
pop %rcx
; 跳转到真实函数
; rax 包含编译后的地址
pop %r10 ; 临时保存原 rax
jmp *%rax
分层编译(Tiered Compilation)是现代 JIT 编译器的核心策略,通过多个编译层次平衡性能和延迟:
第0层:解释器或基线编译器
↓ (执行计数 > 阈值1)
第1层:快速编译,-O0 级别
↓ (执行计数 > 阈值2)
第2层:优化编译,-O2 级别
↓ (成为热点)
第3层:激进优化,-O3 + PGO
虽然 LLVM 本身不强制分层策略,但 ORC JIT 提供了构建分层系统的所有组件:
class TieredJIT {
// 不同优化级别的编译层
IRCompileLayer BaselineLayer; // -O0
IRCompileLayer OptimizedLayer; // -O2
IRCompileLayer AggressiveLayer; // -O3
// 执行计数器
std::map<StringRef, uint64_t> ExecutionCounts;
void handleFunctionCall(StringRef Name) {
ExecutionCounts[Name]++;
if (ExecutionCounts[Name] == TIER1_THRESHOLD) {
promoteToTier1(Name);
} else if (ExecutionCounts[Name] == TIER2_THRESHOLD) {
promoteToTier2(Name);
}
}
void promoteToTier2(StringRef Name) {
// 使用更高级的优化重新编译
auto M = cloneModule(Name);
// 应用 O2 级别优化
PassManagerBuilder PMB;
PMB.OptLevel = 2;
PMB.populateModulePassManager(MPM);
MPM.run(*M);
// 替换函数实现
OptimizedLayer.add(std::move(M));
}
};
JIT 编译的独特优势是可以利用运行时信息进行优化:
class ProfileGuidedJIT {
// 收集的运行时信息
struct ProfileData {
std::map<BasicBlock*, uint64_t> BlockFrequencies;
std::map<CallInst*, Function*> CallTargets;
std::map<BranchInst*, bool> BranchDirections;
};
void optimizeWithProfile(Module &M, const ProfileData &PD) {
// 1. 基于块频率的内联决策
for (auto &F : M) {
for (auto &BB : F) {
if (PD.BlockFrequencies[&BB] > HOT_THRESHOLD) {
// 激进内联热点路径上的调用
aggressiveInline(BB);
}
}
}
// 2. 虚函数去虚化
for (auto &[CI, Target] : PD.CallTargets) {
if (isIndirectCall(CI) && isMonomorphic(CI, Target)) {
// 将间接调用转换为直接调用
devirtualize(CI, Target);
}
}
// 3. 分支预测优化
for (auto &[BI, Direction] : PD.BranchDirections) {
if (getBranchProbability(BI, Direction) > 0.95) {
// 为高概率分支生成更优代码
optimizePredictableBranch(BI, Direction);
}
}
}
};
## 11.6 高级话题:Speculative Compilation 与 Profile-Guided JIT 优化
### 11.6.1 投机编译的设计理念
投机编译(Speculative Compilation)是现代 JIT 编译器的前沿技术,通过预测性地编译可能执行的代码路径来隐藏编译延迟。
#### ORC JIT 的投机编译框架
Lang Hames 在 ORC JIT 中实现了完整的投机编译支持:
```cpp
class SpeculativeJIT {
// 编译队列
std::queue<CompileTask> SpeculativeQueue;
// 热度追踪器
HotnessTracker Tracker;
void recordCall(Function *Caller, Function *Callee) {
Tracker.recordEdge(Caller, Callee);
// 预测可能的调用目标
auto Candidates = predictLikelyTargets(Callee);
for (auto *F : Candidates) {
if (!isCompiled(F) && !isQueued(F)) {
// 加入投机编译队列
SpeculativeQueue.push({F, Priority::SPECULATIVE});
}
}
}
// 基于机器学习的预测
std::vector<Function*> predictLikelyTargets(Function *F) {
// 使用历史数据和程序结构预测
auto Features = extractFeatures(F);
auto Predictions = MLModel.predict(Features);
return getTopK(Predictions, 3);
}
};
Profile-Guided Optimization (PGO) 在 JIT 环境中具有独特优势:
class ProfileGuidedJIT {
struct EdgeProfile {
uint64_t count;
double probability;
};
// 动态去优化和重编译
void recompileWithProfile(Function *F) {
// 1. 收集运行时数据
auto Profile = collectRuntimeProfile(F);
// 2. 注入 profile 元数据
injectProfileMetadata(F, Profile);
// 3. 应用 PGO 优化
PassManagerBuilder PMB;
PMB.OptLevel = 3;
PMB.PGOInstrUse = Profile;
// 4. 重新编译
auto NewAddr = recompile(F, PMB);
// 5. 原子替换函数指针
atomicReplace(F, NewAddr);
}
// 自适应内联决策
bool shouldInline(CallSite CS, const Profile &P) {
// 基于实际调用频率的内联决策
auto CallFreq = P.getCallFrequency(CS);
auto CalleeSize = CS.getCallee()->size();
// 动态调整内联阈值
double Threshold = BaseThreshold * (1.0 + log(CallFreq));
return CalleeSize < Threshold;
}
};
当投机假设失败时,需要去优化(Deoptimization):
class DeoptimizationManager {
// On-Stack Replacement (OSR) 实现
void performOSR(ExecutionContext *Ctx,
DeoptReason Reason) {
// 1. 暂停执行
Ctx->suspend();
// 2. 提取执行状态
auto State = extractExecutionState(Ctx);
// 3. 生成去优化版本
auto *FallbackCode = generateFallback(State);
// 4. 替换栈帧
replaceStackFrame(Ctx, FallbackCode, State);
// 5. 恢复执行
Ctx->resume();
}
// 保护点插入
void insertSafepoints(Function *F) {
for (auto &BB : *F) {
if (isLoopHeader(&BB)) {
// 在循环头插入保护点
IRBuilder<> Builder(&BB, BB.begin());
Builder.CreateCall(SafepointPoll);
}
}
}
};
Azul Systems 的 Falcon JIT(基于 LLVM)引入了多项创新:
// Falcon 的分层编译策略
class FalconTieredCompilation {
enum Tier {
INTERPRETER = 0,
BASELINE_JIT = 1,
OPTIMIZED_JIT = 2,
FULLY_OPTIMIZED = 3
};
void promoteTier(Method *M) {
auto CurrentTier = getTier(M);
auto NextTier = Tier(CurrentTier + 1);
switch (NextTier) {
case BASELINE_JIT:
// 快速编译,最小优化
compileBaseline(M);
break;
case OPTIMIZED_JIT:
// 标准优化 + 内联
compileOptimized(M);
break;
case FULLY_OPTIMIZED:
// 激进优化 + 投机优化
compileAggressive(M);
break;
}
}
};
本章深入探讨了 LLVM 在 JIT 编译中的应用,主要内容包括:
架构演进:从 Legacy JIT 到 MCJIT,再到革命性的 ORC JIT,LLVM 的 JIT 架构不断演进以满足日益复杂的需求。
LLVM JIT 的成功不仅在于技术创新,更在于其模块化设计让各种语言和系统能够轻松集成高性能 JIT 编译能力。
练习 11.1:解释 MCJIT 相比 Legacy JIT 的主要改进,以及为什么最终还是需要 ORC JIT?
练习 11.2:设计一个简单的延迟编译系统,说明桩函数如何触发实际编译。
练习 11.3:比较 Julia 和传统动态语言(如 Python)的执行模型,解释 Julia 为什么能达到 C 的性能。
练习 11.4:设计一个 Profile-Guided JIT 系统,如何收集 profile 数据并触发重编译?考虑性能开销和准确性的权衡。
练习 11.5:分析数据库查询 JIT 编译的适用场景。什么类型的查询最受益于 JIT?什么情况下 JIT 反而有害?
练习 11.6:设计一个支持投机编译的 JIT 系统,如何预测哪些函数值得提前编译?如何处理预测错误?
练习 11.7:比较 ORC JIT 的远程 JIT 功能与 WebAssembly 的执行模型,两者在跨进程/跨机器执行上有何异同?
练习 11.8:Julia 的”首次运行慢”问题的根本原因是什么?除了预编译,还有哪些可能的解决方案?
// 错误:过早触发 JIT 编译
void executeQuery(Query q) {
compileQuery(q); // 每次都编译
runQuery(q);
}
// 正确:基于成本模型
void executeQuery(Query q) {
if (shouldCompile(q)) {
auto compiled = getCachedOrCompile(q);
runCompiled(compiled);
} else {
interpret(q);
}
}
// 错误:不考虑代码内存的特殊性
void* allocateCode(size_t size) {
return malloc(size); // 不可执行!
}
// 正确:使用可执行内存
void* allocateCode(size_t size) {
return mmap(nullptr, size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
}
// 错误:非线程安全的符号解析
Function* resolve(Symbol s) {
if (auto *F = Cache[s])
return F;
// 竞态条件!
auto *F = compile(s);
Cache[s] = F;
return F;
}
// 正确:使用并发安全的解析
Function* resolve(Symbol s) {
std::lock_guard<std::mutex> Lock(CacheMutex);
if (auto *F = Cache[s])
return F;
auto *F = compile(s);
Cache[s] = F;
return F;
}
// 错误:盲目信任 profile 数据
if (profile.getFrequency(BB) > 0) {
// 可能是噪声或过时数据
markAsHot(BB);
}
// 正确:考虑统计显著性
if (profile.getFrequency(BB) > threshold &&
profile.getSampleCount(BB) > minSamples &&
profile.getAge() < maxAge) {
markAsHot(BB);
}
// 错误:无限增长的缓存
CompiledCache[key] = compile(module);
// 正确:有界缓存with LRU
if (CompiledCache.size() >= maxSize) {
evictLRU();
}
CompiledCache[key] = compile(module);
updateAccessTime(key);