使用libgccjit为玩具解释器添加JIT编译
摘要
本教程演示了如何使用libgccjit为简单的基于栈的解释器添加JIT编译,包括代码示例和解释。
<p><a href="https://lobste.rs/s/pkktjt/adding_jit_compilation_toy_interpreter">评论</a></p>
查看缓存全文
缓存时间: 2026/08/24 05:27
# 为玩具解释器添加JIT编译 — libgccjit 17.0.0(实验性)文档
来源:https://gcc.gnu.org/onlinedocs/jit/intro/tutorial04.html
## 教程第4部分:为玩具解释器添加JIT编译¶
(https://gcc.gnu.org/onlinedocs/jit/intro/tutorial04.html#tutorial-part-4-adding-jit-compilation-to-a-toy-interpreter)
在本示例中,我们将构建一个“玩具”解释器,并为其添加JIT编译功能。它是一个基于栈的解释器,旨在作为(非常简单的)示例,展示Python、Ruby等动态语言中常见的字节码解释器类型。
为简单起见,我们的玩具虚拟机功能非常有限:
> - 唯一的数据类型是 `int`
> - 它一次只能处理一个函数(因此只能进行递归调用)
> - 函数只能接受一个参数
> - 函数有一个存储 `int` 值的栈
> - 我们将在解释器中通过调用实现中的函数来实现函数调用,而不是实现自己的栈帧
> - 解析器仅够支持示例运行
当然,真正的解释器会比这复杂得多。
支持以下操作:
| 操作 | 含义 | 旧栈 | 新栈 |
|------|------|------|------|
| `DUP` | 复制栈顶元素 | `[..., x]` | `[..., x, x]` |
| `ROT` | 交换栈顶两个元素 | `[..., x, y]` | `[..., y, x]` |
| `BINARY_ADD` | 将栈顶两个元素相加 | `[..., x, y]` | `[..., (x+y)]` |
| `BINARY_SUBTRACT` | 相减(同上) | `[..., x, y]` | `[..., (x-y)]` |
| `BINARY_MULT` | 相乘(同上) | `[..., x, y]` | `[..., (x*y)]` |
| `BINARY_COMPARE_LT` | 比较栈顶两个元素,若 `x < y` 则压入非零值/零 | `[..., x, y]` | `[..., (x<y)]` |
| `RECURSE` | 递归调用当前函数 | `[..., x]` | `[..., result]` |
| `RETURN` | 从函数返回栈顶值 | `[..., x]` | `x` |
| `PUSH_CONST` | 压入常量 | `[..., ...]` | `[..., ..., const]` |
| `JUMP_ABS_IF_TRUE` | 若栈顶值为真,则跳转到指定绝对地址 | `[..., x]` | `[..., ...]` |
### 示例:一个递归阶乘实现
大致等同于:
```c
int factorial (int arg)
{
if (arg < 2)
return arg;
return arg * factorial (arg - 1);
}
```
初始状态:
```
栈: [arg]
0: DUP
栈: [arg, arg]
1: PUSH_CONST 2
栈: [arg, arg, 2]
2: BINARY_COMPARE_LT
栈: [arg, (arg < 2)]
3: JUMP_ABS_IF_TRUE 9
栈: [arg]
4: DUP
栈: [arg, arg]
5: PUSH_CONST 1
栈: [arg, arg, 1]
6: BINARY_SUBTRACT
栈: [arg, (arg - 1)]
7: RECURSE
栈: [arg, factorial(arg - 1)]
8: BINARY_MULT
栈: [arg * factorial(arg - 1)]
9: RETURN
```
解释器是一个简单的无限循环,包含一个基于下一个操作码的大型 `switch` 语句:
> ```c
> static int toyvm_function_interpret (toyvm_function *fn, int arg, FILE *trace)
> {
> toyvm_frame frame;
>
> #define PUSH(ARG) (toyvm_frame_push (&frame, (ARG)))
> #define POP(ARG) (toyvm_frame_pop (&frame))
>
> frame.frm_function = fn;
> frame.frm_pc = 0;
> frame.frm_cur_depth = 0;
> PUSH (arg);
>
> while (1)
> {
> toyvm_op *op;
> int x, y;
>
> assert (frame.frm_pc < fn->fn_num_ops);
> op = &fn->fn_ops[frame.frm_pc++];
>
> if (trace)
> {
> toyvm_frame_dump_stack (&frame, trace);
> toyvm_function_disassemble_op (fn, op, frame.frm_pc, trace);
> }
>
> switch (op->op_opcode)
> {
> /* 无操作数的指令。 */
> case DUP:
> x = POP ();
> PUSH (x);
> PUSH (x);
> break;
>
> case ROT:
> y = POP ();
> x = POP ();
> PUSH (y);
> PUSH (x);
> break;
>
> case BINARY_ADD:
> y = POP ();
> x = POP ();
> PUSH (x + y);
> break;
>
> case BINARY_SUBTRACT:
> y = POP ();
> x = POP ();
> PUSH (x - y);
> break;
>
> case BINARY_MULT:
> y = POP ();
> x = POP ();
> PUSH (x * y);
> break;
>
> case BINARY_COMPARE_LT:
> y = POP ();
> x = POP ();
> PUSH (x < y);
> break;
>
> case RECURSE:
> x = POP ();
> x = toyvm_function_interpret (fn, x, trace);
> PUSH (x);
> break;
>
> case RETURN:
> return POP ();
>
> /* 带操作数的指令。 */
> case PUSH_CONST:
> PUSH (op->op_operand);
> break;
>
> case JUMP_ABS_IF_TRUE:
> x = POP ();
> if (x)
> frame.frm_pc = op->op_operand;
> break;
>
> default:
> assert (0); /* 未知操作码 */
> }
> /* 操作码 switch 结束 */
> }
> /* while 循环结束 */
>
> #undef PUSH
> #undef POP
> }
> ```
## 编译为机器码¶
(https://gcc.gnu.org/onlinedocs/jit/intro/tutorial04.html#compiling-to-machine-code)
我们希望生成可转换为以下类型并直接在进程内执行的机器码:
> ```c
> typedef int (*toyvm_compiled_code) (int);
> ```
代码的生命周期与 `gcc_jit_result` (https://gcc.gnu.org/onlinedocs/jit/topics/compilation.html#c.gcc_jit_result) * 相关联。我们将通过将它们捆绑到一个结构中来处理,以便我们可以通过调用 `gcc_jit_result_release` (https://gcc.gnu.org/onlinedocs/jit/topics/compilation.html#c.gcc_jit_result_release) 来一起清理它们:
> ```c
> struct toyvm_compiled_function
> {
> gcc_jit_result *cf_jit_result;
> toyvm_compiled_code cf_code;
> };
> ```
我们的编译器不是很复杂;它获取上述每个操作码的实现,并将其直接映射到 libgccjit API 支持的操作。
如何处理栈?理论上我们可以计算每个操作码时的栈深度,并“手动”优化掉栈操作。我们将在下面看到 libgccjit 能够为我们完成此操作,因此我们将通过创建生成函数内部的局部 `stack` 数组和 `stack_depth` 变量来直接实现栈操作,等效于以下 C 代码:
```c
int stack_depth;
int stack[MAX_STACK_DEPTH];
```
我们还将为实现操作码时使用的 `x` 和 `y` 拥有局部变量,等效于:
```c
int x;
int y;
```
这意味着我们的编译器具有以下状态:
> ```c
> struct compilation_state
> {
> gcc_jit_context *ctxt;
>
> gcc_jit_type *int_type;
> gcc_jit_type *bool_type;
> gcc_jit_type *stack_type; /* int[MAX_STACK_DEPTH] */
>
> gcc_jit_rvalue *const_one;
>
> gcc_jit_function *fn;
> gcc_jit_param *param_arg;
>
> gcc_jit_lvalue *stack;
> gcc_jit_lvalue *stack_depth;
>
> gcc_jit_lvalue *x;
> gcc_jit_lvalue *y;
>
> gcc_jit_location *op_locs[MAX_OPS];
> gcc_jit_block *initial_block;
> gcc_jit_block *op_blocks[MAX_OPS];
> };
> ```
## 设置事物¶
(https://gcc.gnu.org/onlinedocs/jit/intro/tutorial04.html#setting-things-up)
首先,我们创建类型:
> ```c
> state.int_type = gcc_jit_context_get_type (state.ctxt, GCC_JIT_TYPE_INT);
> state.bool_type = gcc_jit_context_get_type (state.ctxt, GCC_JIT_TYPE_BOOL);
> state.stack_type = gcc_jit_context_new_array_type (state.ctxt, NULL, state.int_type, MAX_STACK_DEPTH);
> ```
以及提取一个有用的 `int` 常量:
> ```c
> state.const_one = gcc_jit_context_one (state.ctxt, state.int_type);
> ```
我们将使用 `stack` 数组和 `stack_depth` 来实现 push 和 pop。以下是用于向块添加语句以实现压入和弹出值的辅助函数:
> ```c
> static void add_push (compilation_state *state, gcc_jit_block *block,
> gcc_jit_rvalue *rvalue, gcc_jit_location *loc)
> {
> /* stack[stack_depth] = RVALUE */
> gcc_jit_block_add_assignment (
> block, loc,
> /* stack[stack_depth] */
> gcc_jit_context_new_array_access (
> state->ctxt, loc,
> gcc_jit_lvalue_as_rvalue (state->stack),
> gcc_jit_lvalue_as_rvalue (state->stack_depth)),
> rvalue);
>
> /* "stack_depth++;". */
> gcc_jit_block_add_assignment_op (
> block, loc,
> state->stack_depth,
> GCC_JIT_BINARY_OP_PLUS,
> state->const_one);
> }
>
> static void add_pop (compilation_state *state, gcc_jit_block *block,
> gcc_jit_lvalue *lvalue, gcc_jit_location *loc)
> {
> /* "--stack_depth;". */
> gcc_jit_block_add_assignment_op (
> block, loc,
> state->stack_depth,
> GCC_JIT_BINARY_OP_MINUS,
> state->const_one);
>
> /* "LVALUE = stack[stack_depth];". */
> gcc_jit_block_add_assignment (
> block, loc,
> lvalue,
> /* stack[stack_depth] */
> gcc_jit_lvalue_as_rvalue (
> gcc_jit_context_new_array_access (
> state->ctxt, loc,
> gcc_jit_lvalue_as_rvalue (state->stack),
> gcc_jit_lvalue_as_rvalue (state->stack_depth))));
> }
> ```
我们将支持在调试器中单步执行生成的代码,因此我们需要为源代码中的每个操作创建一个 `gcc_jit_location` (https://gcc.gnu.org/onlinedocs/jit/topics/locations.html#c.gcc_jit_location) 实例。这些将引用例如 `factorial.toy` 的行。
> ```c
> for (pc = 0; pc < fn->fn_num_ops; pc++)
> {
> toyvm_op *op = &fn->fn_ops[pc];
> state.op_locs[pc] = gcc_jit_context_new_location (state.ctxt,
> fn->fn_filename,
> op->op_linenum, 0); /* column */
> }
> ```
让我们创建函数本身。像往常一样,我们首先创建其参数,然后使用参数来创建函数:
> ```c
> state.param_arg = gcc_jit_context_new_param (state.ctxt, state.op_locs[0],
> state.int_type, "arg");
> state.fn = gcc_jit_context_new_function (state.ctxt, state.op_locs[0],
> GCC_JIT_FUNCTION_EXPORTED,
> state.int_type,
> funcname,
> 1, &state.param_arg, 0);
> ```
我们在函数内部创建局部变量。
> ```c
> state.stack = gcc_jit_function_new_local (state.fn, NULL, state.stack_type, "stack");
> state.stack_depth = gcc_jit_function_new_local (state.fn, NULL, state.int_type, "stack_depth");
> state.x = gcc_jit_function_new_local (state.fn, NULL, state.int_type, "x");
> state.y = gcc_jit_function_new_local (state.fn, NULL, state.int_type, "y");
> ```
## 填充函数¶
(https://gcc.gnu.org/onlinedocs/jit/intro/tutorial04.html#populating-the-function)
有一些一次性初始化,并且 API 将你创建的第一个块视为函数的入口点,因此我们需要首先创建该块:
> ```c
> state.initial_block = gcc_jit_function_new_block (state.fn, "initial");
> ```
我们现在可以为每个操作创建块。当优化器运行时,其中大多数将被合并到更大的块中。
> ```c
> for (pc = 0; pc < fn->fn_num_ops; pc++)
> {
> char buf[100];
> sprintf (buf, "instr%i", pc);
> state.op_blocks[pc] = gcc_jit_function_new_block (state.fn, buf);
> }
> ```
现在我们有了一个块可以在完成时跳转到,我们可以填充初始块:
> ```c
> /* "stack_depth = 0;". */
> gcc_jit_block_add_assignment (
> state.initial_block, state.op_locs[0],
> state.stack_depth,
> gcc_jit_context_zero (state.ctxt, state.int_type));
>
> /* "PUSH (arg);". */
> add_push (&state, state.initial_block,
> gcc_jit_param_as_rvalue (state.param_arg), state.op_locs[0]);
>
> /* ...并跳转到指令 0。 */
> gcc_jit_block_end_with_jump (state.initial_block, state.op_locs[0], state.op_blocks[0]);
> ```
我们现在可以为各个操作填充块。我们循环遍历它们,向其块中添加指令:
> ```c
> for (pc = 0; pc < fn->fn_num_ops; pc++)
> {
> gcc_jit_location *loc = state.op_locs[pc];
> gcc_jit_block *block = state.op_blocks[pc];
> gcc_jit_block *next_block = (pc < fn->fn_num_ops ? state.op_blocks[pc + 1] : NULL);
> toyvm_op *op;
>
> op = &fn->fn_ops[pc];
> ```
我们将为实现操作码再使用一个大的 `switch` 语句,这次是为编译它们,而不是解释它们。拥有用于实现 push 和 pop 的宏会很有帮助,这样我们可以让即将到来的 `switch` 语句在解释器中看起来尽可能像上面的那个:
```c
#define X_EQUALS_POP() \
add_pop (&state, block, state.x, loc)
#define Y_EQUALS_POP() \
add_pop (&state, block, state.y, loc)
#define PUSH_RVALUE(RVALUE) \
add_push (&state, block, (RVALUE), loc)
#define PUSH_X() \
PUSH_RVALUE (gcc_jit_lvalue_as_rvalue (state.x))
#define PUSH_Y() \
PUSH_RVALUE (gcc_jit_lvalue_as_rvalue (state.y))
```
**注意**:特别巧妙的实现会使用一些预处理器“魔法”让解释器和编译器共享一个*完全相同*的 `switch` 语句。为了简单起见,我们这里没有这样做。
当我最初实现这个编译器时,我在复制粘贴 `Y_EQUALS_POP` 宏时意外地遗漏了一次编辑,导致将栈弹出到 `y` 而错误地赋值给 `x`,使得 `y` 未初始化。
要跟踪此类问题,我们可以使用 `gcc_jit_block_add_comment` (https://gcc.gnu.org/onlinedocs/jit/topics/functions.html#c.gcc_jit_block_add_comment) 为内部表示添加描述性注释。在查看生成的 IR(例如 `factorial`)时,这非常有价值:
> ```c
> gcc_jit_block_add_comment (block, loc, opcode_names[op->op_opcode]);
> ```
我们现在可以编写实现各个操作码的大 `switch` 语句,向相关块填充语句:
> ```c
> switch (op->op_opcode)
> {
> case DUP:
> X_EQUALS_POP ();
> PUSH_X ();
> PUSH_X ();
> break;
>
> case ROT:
> Y_EQUALS_POP ();
> X_EQUALS_POP ();
> PUSH_Y ();
> PUSH_X ();
> break;
>
> case BINARY_ADD:
> Y_EQUALS_POP ();
> X_EQUALS_POP ();
> PUSH_RVALUE (
> gcc_jit_context_new_binary_op (
> state.ctxt, loc,
> GCC_JIT_BINARY_OP_PLUS,
> state.int_type,
> gcc_jit_lvalue_as_rvalue (state.x),
> gcc_jit_lvalue_as_rvalue (state.y)));
> break;
>
> case BINARY_SUBTRACT:
> Y_EQUALS_POP ();
> X_EQUALS_POP ();
> PUSH_RVALUE (
> gcc_jit_context_new_binary_op (
> state.ctxt, loc,
> GCC_JIT_BINARY_OP_MINUS,
> state.int_type,
> gcc_jit_lvalue_as_rvalue (state.x),
> gcc_jit_lvalue_as_rvalue (state.y)));
> break;
>
> case BINARY_MULT:
> Y_EQUALS_POP ();
> X_EQUALS_POP ();
> PUSH_RVALUE (
> gcc_jit_context_new_binary_op (
> state.ctxt, loc,
> GCC_JIT_BINARY_OP_MULT,
> state.int_type,
> gcc_jit_lvalue_as_rvalue (state.x),
> gcc_jit_lvalue_as_rvalue (state.y)));
> break;
>
> case BINARY_COMPARE_LT:
> Y_EQUALS_POP ();
> X_EQUALS_POP ();
> PUSH_RVALUE (
> /* 将 bool 转换为 int */
> gcc_jit_context_new_cast (
> state.ctxt, loc,
> /* (x < y) 作为 bool */
> gcc_jit_context_new_comparison (
> state.ctxt, loc,
> GCC_JIT_COMPARISON_LT,
> gcc_jit_lvalue_as_rvalue (state.x),
> gcc_jit_lvalue_as_rvalue (state.y)),
> state.int_type));
> break;
>
> case RECURSE:
> {
> X_EQUALS_POP ();
> gcc_jit_rvalue *arg = gcc_jit_lvalue_as_rvalue (state.x);
> PUSH_RVALUE (
> gcc_jit_context_new_call (
> state.ctxt, loc,
> state.fn,
> 1, &arg));
> break;
> }
>
> case RETURN:
> X_EQUALS_POP ();
> gcc_jit_block_end_with_return (
> block, loc, gcc_jit_lvalue_as_rvalue (state.x));
> break;
>
> /* 带操作数的指令。 */
> case PUSH_CONST:
> PUSH_RVALUE (
> gcc_jit_context_new_rvalue_from_int (
> state.ctxt, state.int_type, op->op_operand));
> break;
>
> case JUMP_ABS_IF_TRUE:
> X_EQUALS_POP ();
> gcc_jit_block_end_with_conditional (
> block, loc,
> /* "(bool)x". */
> gcc_jit_context_new_cast (
> state.ctxt, loc,
> gcc_jit_lvalue_as_rvalue (state.x),
> state.bool_type),
> state.op_blocks[op->op_operand], /* on_true */
> next_block); /* on_false */
> break;
>
> default:
> assert(0);
> }
> /* 操作码 switch 结束 */
> ```
每个块必须通过调用某个 `gcc_jit_block_end_with_` 入口点来终止。对于两个操作码已经完成此操作,但对于其他操作码,我们需要通过跳转到下一个块来完成。
> ```c
> if (op->op_opcode != JUMP_ABS_IF_TRUE && op->op_opcode != RETURN)
> gcc_jit_block_end_with_jump (block, loc, next_block);
> ```
这类似于简单地增加程序计数器。
## 验证控制流图¶
(https://gcc.gnu.org/onlinedocs/jit/intro/tutorial04.html#verifying-the-control-flow-graph)
完成循环遍历块后,上下文就完整了。与之前一样,我们可以通过使用 `gcc_jit_function_dump_to_dot` (https://gcc.gnu.org/onlinedocs/jit/topics/functions.html#c.gcc_jit_function_dump_to_dot) 来验证控制流和语句是否合理:
```c
gcc_jit_function_dump_to_dot (state.fn, "/tmp/factorial.dot");
```
并查看结果。注意标签名称、注释和变量名称如何显示在转储中,以帮助发现编译器中的错误。
> 控制流图图像
## 编译上下文¶
(https://gcc.gnu.org/onlinedocs/jit/intro/tutorial04.html#compiling-the-context)
完成循环遍历块并用语句填充它们后,上下文就完整了。我们现在可以编译它,并从结果中提取机器码:
> ```c
> gcc_jit_result *jit_result = gcc_jit_context_compile (state.ctxt);
> gcc_jit_context_release (state.ctxt);
>
> toyvm_compiled_function *toyvm_result = (toyvm_compiled_function *)calloc (1, sizeof (toyvm_compiled_function));
> if (!toyvm_result)
> {
> fprintf (stderr, "out of memory allocating toyvm_compiled_function\n");
> gcc_jit_result_release (jit_result);
> return NULL;
> }
> toyvm_result->cf_jit_result = jit_result;
> toyvm_result->cf_code = (toyvm_compiled_code)gcc_jit_result_get_code (jit_result, funcname);
> ```
我们现在可以运行结果:
> ```c
> toyvm_compiled_function *compiled_fn = toyvm_function_compile (fn);
> toyvm_compiled_code code = compiled_fn->cf_code;
> printf ("compiler result: %d\n", code (atoi (argv[2])));
> gcc_jit_result_release (compiled_fn->cf_jit_result);
> free (compiled_fn);
> ```
## 单步执行生成的代码¶
(https://gcc.gnu.org/onlinedocs/jit/intro/tutorial04.html#single-stepping-through-the-generated-code)
相似文章
JIT编译代码在5μs内完成
文章探讨了AI辅助如何简化了创建具有亚微秒级编译时间的快速JIT编译器的过程,并通过一个基于Rust的正则表达式引擎示例进行了演示。
WATaBoy:将Game Boy指令即时编译为Wasm,性能超越原生解释器
本文介绍了WATaBoy,一个Game Boy模拟器,它使用即时编译到WebAssembly的方式,实现了超越原生解释器的性能,是JIT到Wasm在模拟领域的一个概念验证。
内联启发式综述
关于方法JIT编译器中内联启发式的综述,讨论了何时进行内联的挑战以及涉及的权衡,并提供了Ruby和Python的示例。
rustc_codegen_jvm: 可生成JVM字节码的Rust编译器后端
rustc_codegen_jvm 是一个自定义的Rust编译器后端,能够生成JVM字节码,从而将Rust代码编译成可在JVM 8+上运行的JAR文件。它支持多种Rust特性,包括控制流、数据结构、特征(traits)和闭包。
Joy 非正式教程
关于 Joy 编程语言的教程,这是一种基于函数组合和组合子的函数式语言,采用后缀表示法和基于栈的执行方式。