YJIT 黑客

代码生成和汇编语言

YJIT 的基本目的是获取 ISEQ 并生成机器代码。

每个 Ruby 字节码的文档可以在 insns.def 中找到。

YJIT 使用这些字节码作为延迟基本块版本化 (LBBV) 中的“基本块”。有关 LBBV 的更多详细信息,请参阅此目录中的 yjit.md。

当前的 YJIT 使用一个简单的汇编器作为后端。每个生成代码的方法都是通过发出机器代码来完成的

# Excerpt of yjit_gen_exit() from yjit_codegen.c, Sept 2021
// Generate an exit to return to the interpreter
static uint32_t
yjit_gen_exit(VALUE *exit_pc, ctx_t *ctx, codeblock_t *cb)
{
    const uint32_t code_pos = cb->write_pos;

    ADD_COMMENT(cb, "exit to interpreter");

    // Generate the code to exit to the interpreters
    // Write the adjusted SP back into the CFP
    if (ctx->sp_offset != 0) {
        x86opnd_t stack_pointer = ctx_sp_opnd(ctx, 0);
        lea(cb, REG_SP, stack_pointer);
        mov(cb, member_opnd(REG_CFP, rb_control_frame_t, sp), REG_SP);
    }

    // Update CFP->PC
    mov(cb, RAX, const_ptr_opnd(exit_pc));
    mov(cb, member_opnd(REG_CFP, rb_control_frame_t, pc), RAX);

稍后将会有一个更复杂的后端。

代码生成与代码执行

当您在上面看到 lea() 调用(“加载有效地址”)时,它并没有运行 LEA x86 指令。它是在第一个参数中的代码块指针处生成一个 LEA 指令。它将在代码块执行时执行该指令。

这很微妙,因为 YJIT 通常会等到要运行方法时才编译它——那时它才能最清楚地了解方法将接收哪些类型的参数。所以它是一个编译时指令,但通常它会延迟到运行时之前才进行编译。

ctx 结构跟踪在编译时关于传递到 Ruby 字节码的参数的已知信息。YJIT 通常会在生成机器代码之前“窥视”预期类型。

内联代码和外联代码

当 YJIT 生成代码时,它需要一个代码指针。在很多情况下,它需要两个,通常称为“cb”(代码块)和“ocb”(外联代码块)。

cb 用于“内联”普通代码,而 ocb 用于“外联”代码,例如退出。内联代码是 Ruby 操作的正常生成代码,而外联代码用于异常和错误情况,例如遇到意外的参数类型并退出到解释器。

外联代码块的目的是将我们认为不常发生的事情放在其他地方。这样我们就可以使内联块中的代码更线性、更紧凑。线性代码,分支越少越好,更容易被 CPU 预测。异常或不支持的操作会导致 YJIT 生成外联代码来处理它。

如果您在 yjit_codegen.c 中搜索 ocb,您可以看到一些生成外联代码的地方。

只有在 RUBY_DEBUG 或 YJIT_STATS 为真时才会收集 YJIT 统计信息。在某些情况下,用于递增 YJIT 统计信息的代码将被生成在外联,尤其是在发生侧向退出时收集这些统计信息。

统计信息和注释

当 RUBY_DEBUG 定义为真值时,YJIT 会将注释输出到生成的机器代码中。这可以使反汇编更容易阅读。当 RUBY_DEBUG 或 YJIT_STATS 被定义并且统计信息处于活动状态(–yjit-stats 或导出 YJIT_STATS=1)时,将生成代码以在运行期间收集统计信息,并在进程退出时打印报告。

进入和退出解释器

YJIT 只有在某个 ISEQ 被执行一定次数(默认 10 次)后才会为其生成机器码。然后,下次解释器调用该 ISEQ 时,它将改为调用生成的机器码版本。如果 YJIT 遇到意外或不支持的操作,它将返回到正常的解释器。

如果 YJIT 返回到解释器,行为将是正确的,但速度会更慢。YJIT 只优化部分操作 - 例如,YJIT 还没有优化 BMETHOD 调用。

当解释器再次调用 YJIT 优化的方法时,控制权将返回到 YJIT 生成的机器码。在 YJIT 生成的代码中花费的时间越多(“YJIT 中的比例”),YJIT 就可以通过其优化节省更多的 CPU 时间。

侧边出口

当 YJIT 编译了一个 ISEQ 并稍后运行它时,有时它会遇到意外情况。它可能会看到与之前类型不同的参数,或者在第一次使用数组时,在哈希上使用方括号。在这些情况下,生成的代码将包含一个在运行时返回到解释器的调用,称为“侧边出口”。

侧边出口作为非内联代码生成。