Tracing JIT 基於以下假設:
Tracing JIT 有下面幾個步驟:
RVVM 為 RISC-V 系統模擬器,具備 Tracing JIT 的功能又號稱比 QEMU 快 10 倍 又是怎麼一回事?
RVVM 的程式碼中也沒看到 CFG 跟 Trace tree,那又是怎麼處理分支跟巢狀迴圈?
真相就是 RVVM 直接跳過 profile,從運行開始就在做 tracing 跟 codegen,直到遇到 jump 或是 branch 再檢查 tracing 是否超過最大長度,如果結束編譯,那麼等到下次執行到同樣指令時,就開始呼叫 JIT 編譯後的機械碼,利用犧牲空間巧妙的迴避掉複雜的 case。
RVVM/cpu 的目錄裡包含所有 RISC-V 的指令處理,每道指令都有它的解碼跟實作,而且每道指令的實作都以 rvjit_{inst}(rds, rs1, rs2, 4);
來紀錄要執行的指令。
接下來分別以一般指令跟跳躍/分支指令為例來說明 RVVM 怎麼 tracing
以下說明 addi
這道指令,其他指令有相似處,差在 load/store 跟 jump/branch 是不同的寫法
追蹤 rvjit_addi
可以看到 RVVM_RVJIT_TRACE
,並把指令跟 size 當作參數傳入,如果 jit 正在編譯,就更新 jit 的 pc offset 也標記目前這道指令尚未結束
來看 rvjit32_addi
如何實作: RVVM 支援 32 跟 64 位元的指令,因此要分別實作 rvjit32_addi
跟 rvjit64_addi
,其中 RVJIT32_IMM_INC
便能產生 32 位元的指令及其實作。
把 addi
帶入到巨集會產生 rvjit32_native_addi
,這就對應到處理器架構對應產生的指令集,像是 arm, x86, risc-v 的 addi
實作。以 x86 為例:
假設經過 if-else 最後是執行 rvjit_x86_lea_addi
便會開始產生機械碼,最後再透過 rvjit_put_code
把指令附加 (append) 到 block
遇到跳躍或分支的時候會檢查是否超過 BRANCH_MAX_BLOCK_SIZE
,PC 則要更新成 offset,即要跳躍過去的地址
RVVM 執行指令的函式為 riscv_emulate
,目前還不清楚檢查 virt_pc 跟 register 的 pc 目的為何,但只要不執行到 riscv_jit_finalize
那麼就會繼續編譯,如果進行 finalize 的話就會把 tracing 到的 block 放到 vm->jtlb
當作快取
如果 block 被 finalize,則 block 透過 riscv_jit_tlb_put
將位置跟 block(機械碼) 放到 jtlb 的當作快取,如果快取滿了就清掉,並結束編譯。
假設已經紀錄好 addi
,接著下一道指令是 and
,執行 and
的時候會呼叫 rvjit_and
,如果還沒執行 finalize,就會紀錄指令到 tracing,如果已經 finalize,vm->jit_compiling
會變 false,這時候會呼叫 riscv_jit_tlb_lookup
當呼叫 riscv_jit_tlb_lookup
時會去找 jtlb 的快取有沒有 jit 編譯的機械碼,有的話就把 vm 傳入 block 當作參數來執行機械碼。
riscv_jit_lookup
則是 pc 不等於 tpc 時就從 virtual addr 找看看,如果這樣也找不到就初始化一個新的 block 繼續紀錄執行的指令,如此便能夠逐步進行 JIT 編譯