mcpField
I
interpreters

interpreters

github.com/mohitmishra786开发269575 次安装18 周前更新
代码仓库

关于 interpreters

interpreters 是面向 Agent 的 C/C++ 字节码解释器和简单 JIT 实现指南,介绍 VM 架构选择与性能技术,涵盖指令分派、值表示和内联缓存。

主要功能

  1. 01栈式与寄存器式 VM 设计
  2. 02switch 与 computed goto 分派
  3. 03标记指针与 NaN 装箱
  4. 04查找操作的内联缓存
  5. 05基础 JIT 编译技术
  6. 06解释器性能分析

适用场景

  1. 01为语言选择 VM 架构
  2. 02提升字节码分派性能
  3. 03设计紧凑的 VM 值表示
  4. 04添加缓存或基础 JIT 编译

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mohitmishra786/low-level-dev-skills --skill interpreters

安装后在 Claude Code 中输入/interpreters确认已加载。

附带文件references/benchmarks.md

详细介绍

解释器与字节码虚拟机

用途

指导 Agent 使用 C/C++ 实现高效的字节码解释器和简单的 JIT:调度策略、虚拟机架构选择及性能模式。

触发条件

  • “如何实现快速的字节码调度循环?”
  • “switch 调度和 computed goto 有什么区别?”
  • “如何实现基于寄存器与基于栈的虚拟机?”
  • “如何为解释器添加基本的 JIT 编译?”
  • “为什么我的解释器很慢?”

工作流

1. 虚拟机架构选择
类型描述示例
基于栈操作数位于值栈中;字节码紧凑JVM、CPython、WebAssembly
基于寄存器操作数位于虚拟寄存器中;指令更少Lua 5+、Dalvik
直接线程每条“指令”都是一次函数调用一些 Forth 实现
延续传递解释器函数返回延续学术研究

**基于栈:**更易于实现,也更容易编译成;代码生成更简单。每个表达式需要更多指令。 **基于寄存器:**调度次数更少;需要在编译器中进行寄存器分配;处理复杂表达式时缓存行为更好。

2. 调度循环策略
switch 调度(最简单的基线方案)
while (1) {
    uint8_t op = *ip++;
    switch (op) {
        case OP_LOAD:  push(constants[*ip++]); break;
        case OP_ADD:   { Value b = pop(); Value a = pop(); push(a + b); } break;
        case OP_HALT:  return;
        // ...
    }
}

问题:switch 会编译成跳转表中的单个间接分支。现代 CPU 可能会频繁误预测,因为所有操作码都使用同一个间接分支。

Computed goto(GCC/Clang 扩展——最快的可移植方案)
// Table of label addresses
static const void *dispatch_table[] = {
    [OP_LOAD]  = &&op_load,
    [OP_ADD]   = &&op_add,
    [OP_HALT]  = &&op_halt,
    // ...
};

#define DISPATCH() goto *dispatch_table[*ip++]

DISPATCH();  // start

op_load:
    push(constants[*ip++]);
    DISPATCH();

op_add: {
    Value b = pop(); Value a = pop(); push(a + b);
    DISPATCH();
}

op_halt:
    return;

每个操作码末尾都有自己的间接分支。CPU 可以针对每个操作码训练分支预测器,大幅提高预测准确率。

注意:&&label 是 GCC/Clang 扩展,并非标准 C。使用 #ifdef __GNUC__ 添加保护,并为其他编译器回退到 switch。

直接线程代码(最激进的方案)

每个字节码字都是一个函数指针或标签地址;虚拟机本身就是取指、解码和执行的循环。

typedef void (*Handler)(VM *vm);

// Bytecode is an array of handlers
Handler bytecode[] = { op_load, op_push_1, op_add, op_halt };

for (int i = 0; ; i++) {
    bytecode[i](vm);
}
3. 值的表示

**带标签指针:**将类型标签存储在指针的低位中(指针的对齐保证至少有 2 个空闲位):

typedef uintptr_t Value;
#define TAG_INT    0x0
#define TAG_FLOAT  0x1
#define TAG_PTR    0x2
#define TAG_MASK   0x3

#define INT_VAL(v)   ((int64_t)(v) >> 2)
#define FLOAT_VAL(v) (*(float*)((v) & ~TAG_MASK))
#define IS_INT(v)    (((v) & TAG_MASK) == TAG_INT)

**NaN 装箱(64 位):**将非 double 值存储在 NaN 位模式中:

// IEEE 754 quiet NaN: exponent all 1s, mantissa != 0
// Use high mantissa bits as type tag, low 48 bits as payload
// Allows pointer/int/bool/nil to fit in a double-sized slot

曾用于 V8、LuaJIT、JavaScriptCore。

4. 栈管理
#define STACK_SIZE 4096
Value stack[STACK_SIZE];
Value *sp = stack;  // stack pointer

#define PUSH(v) (*sp++ = (v))
#define POP()   (*--sp)
#define TOP()   (sp[-1])
#define PEEK(n) (sp[-(n)-1])

// Check for overflow
#define PUSH_SAFE(v) do { \
    if (sp >= stack + STACK_SIZE) { vm_error("stack overflow"); } \
    PUSH(v); \
} while(0)
5. 内联缓存(IC)

内联缓存会缓存每个调用点最近观察到的类型,从而加速属性查找和方法调度。

struct CallSite {
    Type   cached_type;      // Last observed receiver type
    void  *cached_method;    // Cached function pointer
    int    miss_count;       // Number of misses
};

void invoke_method(VM *vm, CallSite *cs, Value receiver, ...) {
    Type t = GET_TYPE(receiver);
    if (t == cs->cached_type) {
        // Cache hit: direct call, no lookup
        cs->cached_method(vm, receiver, ...);
    } else {
        // Cache miss: look up, update cache
        void *method = lookup_method(t, name);
        cs->cached_type = t;
        cs->cached_method = method;
        cs->miss_count++;
        method(vm, receiver, ...);
    }
}

多态内联缓存(PIC):缓存最多 N 组类型-方法对(通常为 4 组)。

6. 简单 JIT(mmap + 机器码)

对于 x86-64:分配可执行内存,写入机器码字节,然后调用它。

#include <sys/mman.h>
#include <string.h>

typedef int (*JitFn)(int a, int b);

JitFn jit_compile_add(void) {
    // x86-64: add rdi, rsi; mov rax, rdi; ret
    static const uint8_t code[] = {
        0x48, 0x01, 0xF7,   // add rdi, rsi
        0x48, 0x89, 0xF8,   // mov rax, rdi
        0xC3                 // ret
    };

    void *mem = mmap(NULL, sizeof(code),
                     PROT_READ | PROT_WRITE | PROT_EXEC,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (mem == MAP_FAILED) return NULL;

    memcpy(mem, code, sizeof(code));

    // On some systems (macOS Apple Silicon): must use mprotect
    // mprotect(mem, sizeof(code), PROT_READ | PROT_EXEC);

    return (JitFn)mem;
}

在 macOS Apple Silicon(M 系列)上:使用 pthread_jit_write_protect_np() 或 MAP_JIT 标志。

7. 性能提示
  1. **调度:**在 GCC/Clang 上优先使用 computed goto,而不是 switch
  2. **值:**使用 NaN 装箱或带标签指针;避免在热点路径中装箱和拆箱
  3. **栈:**将栈指针保存在被调用者保存的寄存器中(register Value *sp asm("r15")——GCC 全局寄存器变量)
  4. **局部变量访问:**将频繁访问的局部变量保存在虚拟机寄存器(结构体字段)中,而不是栈上
  5. **性能分析:**使用 perf 或采样来定位调度开销与实际工作量
  6. **特化:**为常见类型组合生成特化处理程序(int+int 加法与通用加法)
  7. **跟踪记录:**跟踪式 JIT(LuaJIT 的方案)会编译热点跟踪,而不是完整函数

如需了解调度策略基准测试,请参阅 references/benchmarks.md。

相关技能

  • 使用 skills/profilers/linux-perf 对解释器调度循环进行性能分析
  • 使用 skills/low-level-programming/assembly-x86 了解 JIT 输出
  • 使用 skills/runtimes/fuzzing 对字节码解析器/加载器进行模糊测试
  • 使用 skills/compilers/llvm 实现基于 LLVM IR 的 JIT(MCJIT / ORC JIT)

评论

支持 Markdown