YJIT Hacking

程式碼生成與組合語言

YJIT 的基本目的是接收 ISEQs 並生成機器碼。

有關每個 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);

以後將會有一個更複雜的後端。

代碼生成 vs 代碼執行

當您在上面看到 lea() 調用(“load effective address”)時,它並不是執行 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 被定義為真值時,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 並在以後運行時,有時會遇到意外條件。它可能會看到與之前不同類型的參數,或者在散列上首次使用方括號時會使用在數組上的方括號。在這些情況下,生成的代碼將包含一個在運行時返回解譯器的調用,稱為“側退出”。

側退出被生成為離線代碼。