C++中的JIT是运行时按需将代码(如表达式、IR)动态编译为机器码并在内存中执行,区别于g++等传统编译器的离线静态编译;其实现可手写x86-64机器码或借助LLVM IR实现。

什么是C++中的JIT,它和普通编译有什么区别?
JIT(Just-In-Time)编译器不是在程序启动前把全部代码编译成机器码,而是在运行时,按需把某段代码(比如一段字符串形式的C++表达式、字节码或自定义IR)动态编译为本地机器指令,并立即执行。它不像g++那样生成可执行文件,而是调用操作系统API(如mmap + mprotect)申请可执行内存,把生成的机器码拷进去,再通过函数指针调用。
关键区别在于:传统编译是“离线+静态”,JIT是“在线+动态+内存中执行”。对C++来说,实现一个“真正能编译C++语法”的JIT极其复杂(需完整前端),所以入门级JIT通常聚焦于:表达式求值、简单函数生成(如f(x) = x*x + 2*x + 1)或LLVM IR → 机器码这一环。
从零写一个表达式JIT:三步核心流程
不依赖LLVM也能动手——用纯C++手写一个支持加减乘除和括号的浮点表达式JIT(基于栈机思想 + x86-64机器码生成):
-
词法分析:把字符串如
"3.14 + x * 2"切分成token(数字、变量名、运算符) - 语法树构建:递归下降解析,生成AST(例如二叉树,叶子是数字/变量,节点是+、*等)
-
机器码生成:遍历AST,按x86-64调用约定(如rdi存x参数),用
movsd、addsd、mulsd等SSE指令生成代码,写入可执行内存
示例片段(简化):
立即学习“C++免费学习笔记(深入)”;
// 假设已分配好 executable_buffer// 生成 "return x * x" 的机器码(x来自rdi)
uint8_t code[] = {
0x48, 0x89, 0xf8, // mov rax, rdi
0xf2, 0x0f, 0x59, 0xc0, // mulsd xmm0, xmm0 (假设x已在xmm0)
0xc3 // ret
};
memcpy(executable_buffer, code, sizeof(code));
mprotect(executable_buffer, size, PROT_READ | PROT_WRITE | PROT_EXEC);
auto func = reinterpret_cast
double result = func(5.0); // → 25.0
更实用的路径:用LLVM构建C++ JIT(推荐入门进阶)
LLVM提供了成熟、跨平台、生产级的JIT基础设施(llvm::orc API)。它不让你手写机器码,而是专注描述计算逻辑(用LLVM IR),由LLVM自动优化并生成机器码。
典型步骤(C++代码):
- 初始化LLVM环境:
InitializeNativeTarget()、InitializeNativeTargetAsmPrinter() - 创建JIT引擎:
orc::KaleidoscopeJIT或orc::ThreadSafeContext+orc::ExecutionSession - 构建模块:用
IRBuilder生成IR(例如创建函数double add(double a, double b)) - 添加到JIT并获取函数指针:
auto sym = jit->lookup("add"); auto f = sym->getAddress();
优势明显:支持循环、条件、函数调用、链接外部符号(如printf)、自动优化(O2)、多模块热重载——这才是工业级JIT的起点。
避坑提醒:权限、ABI与调试
新手常卡在这几个地方:
-
内存不可执行:Linux/macOS需
mprotect(..., PROT_EXEC);Windows用VirtualProtect(..., PAGE_EXECUTE_READWRITE) - 调用约定错位:x86-64下整数参数走rdi/rsi/rdx,浮点走xmm0/xmm1;返回值:整数在rax,double在xmm0
-
缺少符号解析:若JIT函数里调用了
sin或malloc,必须提前用addGlobalMapping注册其地址 -
调试困难:可用
llvm::sys::PrintStackTrace()捕获崩溃;生成.bc文件用llc离线反汇编验证IR→asm是否合理
不复杂但容易忽略。











