其他
在 C/C++ 中实现或优化字节码 VM 与简单 JIT…
需要设计字节码分发循环、栈式/寄存器式 VM、内联缓存或 mmap 简单 JIT 时,用这套做法落地高性能解释器。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 C/C++ 中实现或优化字节码 VM通用
解决思路
- 选择 VM 架构:栈式 VM 把操作数放在值栈上,字节码紧凑,易于实现和编译生成,但每个表达式指令更多;参考 JVM、CPython、WebAssembly。
- 选择 VM 架构:寄存器式 VM 把操作数放在虚拟寄存器中,减少分发迭代,复杂表达式缓存行为更好,但编译器需要做寄存器分配;参考 Lua 5+、Dalvik。
- 选择 VM 架构:直接线程化把每条“指令”做成函数调用,可参考部分 Forth 实现。
- 选择 VM 架构:续延传递风格让解释器函数返回续延,偏学术用途。
- 实现 switch dispatch 作为最简单基线:用 while(1) { uint8_t op = *ip++; switch(op){...} },各 case 执行操作并在 OP_HALT 时 return。
- 注意 switch dispatch 的问题:它编译成一个跳转表间接分支,所有 opcode 共用,现代 CPU 可能严重误预测。
- 实现 computed goto(GCC/Clang 扩展,最快的可移植方案):定义 static const void *dispatch_table[],用 [OP_LOAD] = &&op_load 等标签地址初始化。
- 在 computed goto 中定义 #define DISPATCH() goto *dispatch_table[*ip++];启动时调用 DISPATCH(),每个 opcode 标签执行后也调用 DISPATCH()。
- computed goto 的每个 opcode 结尾使用自己的间接分支,CPU 可按 opcode 训练分支预测器,显著提高预测率。
- 注意 &&label 是 GCC/Clang 扩展,非标准 C;用 #ifdef __GNUC__ 保护,其他编译器回退到 switch。
- 实现 direct threaded code(最激进):每个字节码字是函数指针或标签地址,VM 本身就是 fetch-decode-execute 循环。
- direct threaded code 可用 typedef void (*Handler)(VM *vm); Handler bytecode[] = { op_load, op_push_1, op_add, op_halt }; 然后 for (int i = 0; ; i++) bytecode[i](vm);。
- 用 tagged pointer 表示值:把类型标签放在指针低位,利用指针对齐保证的低位空闲位。
- tagged pointer 可定义 TAG_INT 0x0、TAG_FLOAT 0x1、TAG_PTR 0x2、TAG_MASK 0x3,以及 INT_VAL(v) ((int64_t)(v) >> 2)、FLOAT_VAL(v) (*(float*)((v) & ~TAG_MASK))、IS_INT(v) (((v) & TAG_MASK) == TAG_INT) 等宏。
- 用 NaN boxing(64 位)表示值:把非 double 值编码进 NaN 位模式。
- NaN boxing 利用 IEEE 754 quiet NaN(指数全 1、尾数 != 0),用尾数高位作为类型标签、低 48 位作为 payload,使 pointer/int/bool/nil 都能放进 double 大小的槽位;V8(早期)、LuaJIT、JavaScriptCore 使用此法。
- 管理栈:定义 #define STACK_SIZE 4096、Value stack[STACK_SIZE]、Value *sp = stack,并用 PUSH(v) (*sp++ = (v))、POP() (*--sp)、TOP() (sp[-1])、PEEK(n) (sp[-(n)-1]) 等宏。
- 栈溢出检查:用 PUSH_SAFE(v) do { if (sp >= stack + STACK_SIZE) { vm_error("stack overflow"); } PUSH(v); } while(0)。
- 实现内联缓存(IC)加速属性查找和方法分发:在每个调用点缓存最近观察到的类型。
- IC 可定义 struct CallSite { Type cached_type; void *cached_method; int miss_count; }。
- 实现 invoke_method(VM *vm, CallSite *cs, Value receiver, ...):取 GET_TYPE(receiver);若 t == cs->cached_type 则直接调用 cs->cached_method;否则 lookup_method(t, name),更新 cs->cached_type、cs->cached_method,miss_count++,再调用 method。
- 实现多态内联缓存(PIC):缓存最多 N 个(通常 4 个)type-method 对。
- 实现简单 JIT(mmap + 机器码):针对 x86-64,分配可执行内存,写入机器码字节,然后调用它。
- 用 mmap(NULL, sizeof(code), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) 分配 JIT 内存;若返回 MAP_FAILED 则返回 NULL;用 memcpy(mem, code, sizeof(code)) 写入代码;返回转换后的函数指针。
- JIT 示例机器码为 add rdi, rsi; mov rax, rdi; ret,字节序列 0x48, 0x01, 0xF7, 0x48, 0x89, 0xF8, 0xC3。
- 在某些系统(macOS Apple Silicon)必须用 mprotect(mem, sizeof(code), PROT_READ | PROT_EXEC) 或 pthread_jit_write_protect_np() 或 MAP_JIT flag。
- 性能:在 GCC/Clang 上用 computed goto 替代 switch dispatch。
- 性能:用 NaN boxing 或 tagged pointer;在热路径避免 boxing/unboxing。
- 性能:把栈指针放在 callee-saved 寄存器中,如 register Value *sp asm("r15")(GCC global register variable)。
- 性能:把频繁访问的局部变量放在 VM 寄存器(struct 字段)中,而不是栈上。
- 性能:用 perf 或采样分析区分分发开销和实际工作开销。
- 性能:为常见类型组合生成特化 handler 变体(如 int+int add 与通用 add)。
- 性能:Trace JIT(LuaJIT 思路)编译热跟踪,而不是编译完整函数。
- 参考分发策略基准:见 references/benchmarks.md。
- 相关技能:用 skills/profilers/linux-perf 分析解释器分发循环。
- 相关技能:用 skills/low-level-programming/assembly-x86 理解 JIT 输出。
- 相关技能:用 skills/runtimes/fuzzing 对字节码 parser/loader 做 fuzz。
- 相关技能:用 skills/compilers/llvm 实现基于 LLVM IR 的 JIT(MCJIT / ORC JIT)。
适用边界 · 注意事项
- 当目标编译器不支持 GCC/Clang 的 &&label/computed goto 扩展且没有 switch fallback 时,不要使用 computed goto。
- 当平台有 W^X/JIT 写保护限制而未处理 mprotect/MAP_JIT/pthread_jit_write_protect_np 时,不要直接映射 RWX 内存执行。
- 当需要生产级、跨平台、带高级优化的 JIT 或完整运行时,不要只靠手写简单 JIT,应考虑 LLVM MCJIT/ORC JIT 等方案。