YJIT 黑客

代码生成和汇编语言

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

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

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 为 true 时,才会收集 YJIT 统计信息。在某些情况下,将生成外联代码以增加 YJIT 统计信息,尤其是在发生侧向退出时收集这些统计信息时。

统计信息和注释

当 RUBY_DEBUG 被定义为 true 值时,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 并稍后运行它时,有时它会遇到一个意外的条件。它可能会看到一个与之前不同类型的参数,或者在哈希上使用了方括号,而最初是在数组上使用的。在这些情况下,生成的代码将包含一个在运行时返回到解释器的调用,称为“侧退出”。

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