LLVM JIT编译技术:从基础原理到现代架构实践
LLVM JIT编译技术:从基础原理到现代架构实践
JIT编译基础概念与核心价值
JIT与AOT对比分析
JIT(Just-In-Time)编译是是一种在程序运行时动态生成和执行代码的技术。与传统的静态编译不同,JIT编译将代码生成过程推迟到运行时,实现了真正的"按需编译"。
// 传统AOT编译:提前生成所有代码
// 编译时:source.cpp → a.out(完整可执行文件)
// 运行时:直接执行预编译的机器码// JIT编译:运行时动态生成代码
// 编译时:source.cpp → bytecode(中间表示)
// 运行时:bytecode → 机器码(按需编译执行)
JIT常被称为懒编译(late/lazy compilation),其核心包含两个关键行为:
- 动态生成代码
- 动态执行生成的代码
AOT(Ahead-Of-Time)编译则是在程序执行之前将源代码(文本、字节码等)预先编译为本地机器代码,运行时直接执行这些预编译的代码。
LLVM JIT核心特性
LLVM JIT编译器基于函数粒度进行编译优化,因为它可以一次只编译一个函数。(理论上可以进一步提高粒度,为 trace,即函数的某条特定执行路径,但还待研究)具备以下核心特性:
- 懒编译机制:仅在函数被调用时才进行编译
- 外部符号解析:支持跨模块的全局变量和函数解析
- 动态库集成:通过dlsym实现与系统动态库的无缝交互
JIT engine 会在运行时编译执行 LLVM IR 函数,在进行编译,它会使用 LLVM code generator(代码生成器)去生成指定平台的二进制指令,然后会返回编译好的函数指针,这样就可以通过函数指针的方式来调用函数了。
JIT的技术优势
AOT编译虽然能够生成高度优化的本地代码,但由于静态编译的特性,无法准确预测代码在运行时的具体行为,难以达到极致的性能优化。
相比之下,JIT技术融合了解释器和AOT的优点:
- 快速启动:初期采用解释执行,避免编译延迟
- 运行时优化:基于实际执行profile生成针对性优化的本地代码
- 自适应优化:根据运行时数据和行为动态调整优化策略
生成更高效的代码是需要花费很多时间的,为了在快速启动和高度优化之间取得平衡,很多 JIT/AOT 实现(比如 Java 虚拟机和 Firefox 浏览器)使用了分层(Tiered)编译技术。分层编译一般分为两层,第一层(tier1,叫法因实现而异)编译器可以较快地生成本地代码(简陋的优化编译,如 -O0)并执行;而第二层(tier2)编译器在后台线程中生成执行效果更好的代码,并替换 tier 生成的代码。
这种解释器+JIT的组合已成为现代运行时系统的主流架构。为了在快速启动和深度优化之间取得平衡,许多JIT实现(如Java HotSpot VM、V8 JavaScript引擎)采用**分层编译(Tiered Compilation)**技术:
JIT编译的技术优势深度分析
运行时优化优势
graph TBA[源代码] --> B[解释器执行]B --> C[收集运行时Profile]C --> D{性能分析}D --> E[热点代码识别]E --> F[JIT编译优化]F --> G[生成优化机器码]G --> H[替换解释代码]H --> I[高性能执行]
JIT编译器通过运行时数据收集,能够做出比AOT更精准的优化决策:
- 基于实际使用模式的优化
- 方法内联:根据实际调用频率决定内联策略
- 虚函数去虚拟化:基于运行时类型信息
- 分支预测优化:根据实际分支概率
- 自适应优化策略
// 分层编译示例:根据使用频率选择优化级别
enum OptimizationLevel {INTERPRETER, // 解释执行,零编译开销QUICK_JIT, // 快速编译,基础优化OPTIMIZING_JIT // 深度优化,生成高质量代码
};OptimizationLevel selectOptLevel(FunctionUsageProfile profile) {if (profile.invocationCount < 10) return INTERPRETER;if (profile.invocationCount < 1000) return QUICK_JIT;return OPTIMIZING_JIT;
}
动态代码更新的革命性价值
热重载(Hot Reload) 在开发调试中提供即时反馈:
class HotReloadManager {
public:Error reloadFunction(const std::string& name, const std::string& newIR) {// 1. 编译新版本函数auto newFunc = compileIR(newIR);// 2. 原子替换函数实现return atomicFunctionSwap(name, newFunc);}
};// 开发工作流:编辑→保存→立即测试
developer.editCode("feature.cpp");
hotReloadManager.reloadFunction("calculateFeature");
developer.testFeature(); // 立即生效,无需重启
热修复(Hot Fix) 在生产环境中的价值:
// 线上问题紧急修复
void emergencyHotFix() {// 从服务器下载修复补丁auto patch = downloadHotFix("critical_bug_patch.bc");// 动态替换有问题的函数jitEngine->replaceFunction("buggyFunction", patch);// 系统继续运行,无需停机部署continueNormalOperation();
}
LLVM JIT 特点
LLVM JIT 系统使用 ExecutionEngine 类来提供支持,用来组织整个程序的执行、分析下一个需要被执行的程序段、选择对应的操作来执行,它会把代码生成到内存当中,但是否要执行取决于用户(开发者是否要调用)。
LLVM JIT engine 有以下特点:
- 懒编译(lazy compilation):只在调用时才进行函数的编译。如果关闭该特性,则获得函数指针时就会马上进行编译。
- 外部全局变量的编译:包括对当前 LLVM 模块(module)之外的实例,进行符号解析和内存分配
- 通过 dlsym 查找和解析外部符号:这是在运行时进行动态共享对象(dynamic shared object,DSO)加载一样的过程
把二进制指令写进内存里是由 ExecutionManager 类完成的,它会把函数指针给用户。内存管理的任务包括内存分配、释放、提供内存空间给库加载、内存权限处理。JIT 和 MCJIT 都实现了自己的内存管理类,继承于 RTDyldMemoryManager 基类。
LLVM JIT引擎架构深度解析
Legacy JIT:技术起点与局限性
Legacy JIT作为LLVM最初的JIT实现,奠定了基础架构模式:
class LegacyJIT {
public:// 基础JIT功能接口void* getPointerToFunction(Function* F);void* getPointerToBasicBlock(BasicBlock* BB);private:// 简单的内存管理和代码缓存JITMemoryManager* MemManager;std::map<Function*, void*> FunctionCache;
};
主要技术限制:
- 全局锁导致并发性能差
- 缺乏模块化设计,难以扩展
- 内存管理策略简单,容易碎片化
主要限制包括:
- 不支持按需编译(必须编译整个模块)
- 缺乏动态优化能力
- 在LLVM 3.1后被MCJIT替代
MCJIT:工业化JIT引擎
MCJIT是LLVM的第二代JIT编译器。它支持许在运行时动态生成和加载代码。
模块状态机设计
MCJIT引入了精细的编译状态管理:
enum ModuleState {MODULE_ADDED, // 模块已添加,可被引用但未编译MODULE_LOADED, // 已编译,内存分配但重定位未完成MODULE_FINALIZED // 完全就绪,可执行状态
};class MCJITModule {ModuleState state;std::unique_ptr<Module> llvmModule;std::vector<JITSymbol> symbols;void* codeBase; // 代码基地址
};
- Added - 还未被编译,但已加入到执行引擎当中。允许模块暴露函数定义给其他模块,然后进行延迟编译,直到需要时。
- Loaded - 该模块处于 JIT编译,但未准备好被执行。重定位未完成,且需要分配合适权限的内存页。用户可以在内存中 remap JIT 编译的函数而不需要重新编译。
- Finalized - 模块包含有准备被执行的函数。不能被 remap,因为重定位已经完成了。这个状态的设计,使得 MCJIT 要获取符号地址时,必须整个模块已经处于 Finalized 状态。MCJIT::finalizeObject() 会调用 generateCodeForModule() 来生成已加载的模块,然后所有的模块会通过 finalizeLoadedModules() 函数被 finalized。
generateCodeForModule() 做了以下几件事情:
- 创建 ObjectBuffer 实例来持有 Module 对象,如果 Module 已经被加载(编译过了),那么会使用 ObjectCache 接口来查找,避免重编。
- 如果没有 cache,那么执行 MC 代码生成 MCJIT::emitObject(),会返回 ObjectBufferStream 对象。
- RuntimeDyld 动态链接器会加载结果 ObjectBuffer 对象(根据文件格式调用对应平台的),然后通过 RuntimeDyld::loadObject() 来创建符号表,返回 ObjectImage 对象。
- 标记模块为已加载 Loaded。
RuntimeDyld 动态链接器是在 Module 进行 finalization (符号解析、注册异常处理)时被使用。MCJIT 也会使用 RuntimeDyld 通过 RuntimeDyld::getSymbolLoadAddress() 函数来查找符号地址。
MCJIT 的一些关键设计包括:Engine Creation,Code Generation,Object Loading,Address Remaping,Final Preparations。
完整的编译工作流程
sequenceDiagramparticipant C as Clientparticipant E as MCJIT Engineparticipant G as Code Generatorparticipant L as RuntimeDyldparticipant M as Memory Managerparticipant Cache as Object CacheC->>E: EngineBuilder(module).create()E->>E: moduleState = MODULE_ADDEDC->>E: getPointerToFunction("foo")E->>Cache: getObject(module)alt Cache HitCache->>E: return cached objectE->>L: load cached objectelse Cache MissE->>G: emitObject(module)G->>G: PassManager.run(module)G->>E: return ObjectBufferE->>Cache: notifyObjectCompiled(module, object)endL->>M: allocateCodeSection(size)M->>L: return allocated memoryL->>L: applyRelocations()L->>M: finalizeMemory()E->>E: moduleState = MODULE_FINALIZEDE->>C: return function pointer
Engine Creation:
MCJIT引擎通常通过EngineBuilder对象创建。EngineBuilder接受一个llvm::Module对象作为构造函数参数,并且可以设置多个参数选项,比如MemoryManager,EngineKind,TargetOptions 等。如果没有显式创建内存管理器,MCJIT引擎实例化时会创建一个默认的内存管理器(通常是SectionMemoryManager)。
std::unique_ptr<ExecutionEngine> ee(EngineBuilder(module.get()).setMCJITMemoryManager(std::make_unique<SectionMemoryManager>()).create());
EngineBuilder::create 将调用静态 MCJIT::createJIT 函数,并传递 Module, MemoryManager, Targetmachine 的指针,所有这些对象随后都将归MCJIT对象所有。
MCJIT 类有一个成员变量 Dyld,其中包含 RuntimeDyld 封装类的实例。该成员将用于 MCJIT 与实际 RuntimeDyldImpl对象之间的通信,该对象将在加载对象时创建。Dyld 构造函数中包含了SymbolResolver和MemoryManager。
MCJIT 在创建时持有了Module的指针,但此时不会立刻触发代码生成,直到调用MCJIT::finalizeObject方法或MCJIT::getPointerToFunction方法才会生成代码。
CodeGeneration
代码生成时,MCJIT首先尝试从其ObjectCache成员检索对象映像。如果无法检索到缓存的对象映像,MCJIT将调用其emitObject方法。(generateCodeForModule)
MCJIT::emitObject使用本地PassManager实例并创建一个新的 ObjectBufferStream 实例,并将这两个实例传递给 TargetMachine::addPassesToEmitMC。最后在Module上调用 PassManager::run。可以参考runtimedyld_example.cpp
legacy::PassManager passManager;
passManager.run(*module);
ObjectLoading
实际上emitObject获得了MemoryBuffer,它会被传递给RuntimeDyld进行加载。RuntimeDyld包装类会检查对象以确定其文件格式,并创建RuntimeDyldELF或RuntimeDyldMachO的实例(两者都派生自RuntimeDyldImpl基类)。
RuntimeDyldImpl::loadObject首先从接收到的 ObjectBuffer 中创建一个 ObjectImage 实例。ObjectImage 封装了 ObjectFile 类,是一个辅助类,用于解析二进制对象映像,并提供对特定格式头信息(包括段、符号和重定位信息)的访问RuntimeDyldImpl::loadObject会遍历映像中的符号。每个函数或数据符号都加载到内存中,并将符号存储到符号表中。
接下来,RuntimeDyldImpl::loadObject会遍历对象映像中的各个部分,并为每个部分遍历该部分的重定位。对于每个重定位,它都会调用特定于格式的 processRelocationRef 方法,该方法将检查重定位并将其存储在基于section的重定位列表映射或外部符号重定位映射。
当RuntimeDyldImpl::loadObject返回时,对象的所有代码和数据部分都已加载到内存管理器分配的内存中,重定位信息也已准备就绪,但重定位尚未应用,生成的代码仍未准备好执行。
Address Remaping
在初始代码生成后和调用finalizeObject之前,客户端可以重映射对象中section的地址。这通常是因为代码是为外部进程生成的,并且正在映射到该进程的地址空间。
客户端通过调用MCJIT::mapSectionAddress来重映射节地址。这应该在节内存被复制到新位置之前完成。
Final Preparations
当调用MCJIT::finalizeObject时,MCJIT调用RuntimeDyld::resolveRelocations方法。这个方法会尝试定位任何外部符号,然后为对象应用所有重定位。
重定位完成后,MCJIT调用RuntimeDyld::getEHFrameSection方法,如果返回非零结果,则将节数据传递给内存管理器的registerEHFrames方法。
最后,MCJIT调用内存管理器的finalizeMemory方法,内存管理器将在必要时使目标代码缓存失效,并对已分配给代码和数据内存的内存页应用最终权限。
内存管理架构
class SectionMemoryManager : public RTDyldMemoryManager {
private:struct AllocatedSection {uint8_t* addr;size_t size;unsigned permissions; // RX, RW, etc.};std::vector<AllocatedSection> codeSections;std::vector<AllocatedSection> dataSections;public:uint8_t* allocateCodeSection(uintptr_t Size, unsigned Alignment,unsigned SectionID, StringRef SectionName) override {// 分配可执行内存auto addr = allocateSection(Size, Alignment, MEM_EXECUTE);codeSections.push_back({addr, Size, MEM_EXECUTE});return addr;}void registerEHFrames(uint8_t* Addr, uint64_t LoadAddr, size_t Size) override {// 注册异常处理帧registerEHFrameSection(Addr, Size);}
};
对象缓存优化
class ObjectCache {
private:std::map<std::string, std::vector<uint8_t>> cache;std::string cacheDirectory;public:void notifyObjectCompiled(const Module* M, MemoryBufferRef ObjBuffer) {// 基于模块内容生成缓存键std::string key = generateModuleHash(M);// 缓存对象文件cache[key] = serializeObjectBuffer(ObjBuffer);// 可选:持久化到磁盘if (!cacheDirectory.empty()) {saveToDisk(key, ObjBuffer);}}std::unique_ptr<MemoryBuffer> getObject(const Module* M) {std::string key = generateModuleHash(M);if (cache.find(key) != cache.end()) {return deserializeToMemoryBuffer(cache[key]);}// 检查磁盘缓存return loadFromDisk(key);}
};
引擎创建与初始化
#include "llvm/ExecutionEngine/MCJIT.h"
#include "llvm/ExecutionEngine/SectionMemoryManager.h"std::unique_ptr<llvm::Module> module = createLLVMModule();
std::string error;// 创建MCJIT执行引擎
llvm::ExecutionEngine* ee = llvm::EngineBuilder(std::move(module)).setEngineKind(llvm::EngineKind::JIT).setMCJITMemoryManager(std::make_unique<llvm::SectionMemoryManager>()).setErrorStr(&error).create();if (!ee) {llvm::errs() << "Failed to create MCJIT: " << error << "\n";return;
}
代码生成与对象加载
// MCJIT内部代码生成流程
class MCJIT {
private:llvm::RTDyldMemoryManager* MemMgr;llvm::RuntimeDyld Dyld;llvm::ObjectCache* ObjCache;public:llvm::ObjectBufferStream* emitObject(llvm::Module* M) {// 检查对象缓存