使用libgccjit为玩具解释器添加JIT编译

Lobsters Hottest 工具

摘要

本教程演示了如何使用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内完成

Hacker News Top

文章探讨了AI辅助如何简化了创建具有亚微秒级编译时间的快速JIT编译器的过程,并通过一个基于Rust的正则表达式引擎示例进行了演示。

内联启发式综述

Lobsters Hottest

关于方法JIT编译器中内联启发式的综述,讨论了何时进行内联的挑战以及涉及的权衡,并提供了Ruby和Python的示例。

rustc_codegen_jvm: 可生成JVM字节码的Rust编译器后端

Lobsters Hottest

rustc_codegen_jvm 是一个自定义的Rust编译器后端,能够生成JVM字节码,从而将Rust代码编译成可在JVM 8+上运行的JAR文件。它支持多种Rust特性,包括控制流、数据结构、特征(traits)和闭包。

Joy 非正式教程

Lobsters Hottest

关于 Joy 编程语言的教程,这是一种基于函数组合和组合子的函数式语言,采用后缀表示法和基于栈的执行方式。