Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ZJITでの最適化: Recompile編 / Wakate.rb #7

ZJITでの最適化: Recompile編 / Wakate.rb #7

Avatar for Nozomi Hijikata

Nozomi Hijikata

June 23, 2026

Other Decks in Technology

Transcript

  1. 1 自己紹介 Nozomi Hijikata (@nozomemein) プロフィール 株式会社Techouse でエンジニアをしています Ruby/Rails 書いたり、Rust

    書いたり、モバイルアプリやったり、インフラやったり… 24 卒 / 1 児のパパ 好きなもの Compiler / 最適化 勉強(古代オリエントにはまってます)
  2. 1

  3. 1 前提 - ZJIT はプロファイルをためてその情報を元に最適化をする e.g. より早い処理で置き換えたり、結果をたたみ込んだり - VM 側からYARV

    の命令列をプロファイル用に一時的に差し替える形でプロファイルをする iseq->body->iseq_encoded[insn_idx] = (VALUE)insn_table[zjit_insn]; // Convert a given ISEQ's instructions to zjit_* instructions void rb_zjit_profile_enable(const rb_iseq_t *iseq) { // This table encodes an opcode into the instruction's address const void *const *insn_table = rb_vm_get_insns_address_table(); unsigned int insn_idx = 0; while (insn_idx < iseq->body->iseq_size) { int insn = rb_vm_insn_addr2opcode((void *)iseq->body->iseq_encoded[insn_idx]); int zjit_insn = vm_bare_insn_to_zjit_insn(insn); if (insn != zjit_insn) { } insn_idx += insn_len(insn); } }
  4. 1 集めたプロファイルを元にそのパスの最適化度合いを決める → @foo = 4 def test defined?(@foo) end

    test;test v10:StringExact|NilClass = DefinedIvar v6, :@foo Initial HIR: fn test@../test.rb:2: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): CheckInterrupts Return v10 v16:HeapBasicObject = GuardType v6, HeapBasicObject v17:CShape = LoadField v16, :shape_id@0x4 v18:CShape[0x80008] = GuardBitEquals v17, CShape(0x80008) v19:StringExact[VALUE(0x103ba9440)] = Const Value(VALUE(0x103ba9440)) Optimized HIR: fn test@../test.rb:2: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): CheckInterrupts Return v19 論点から逸れるのでここでは最適化の中身自体は触れませんが、Compile 時の情報を元に結果を畳み込んでいると思ってください
  5. 1 もし仮に前提条件を壊すパターンがくるとどうなる? v18:CShape[0x80008] = GuardBitEquals v17, CShape(0x80008) Optimized HIR: fn

    test@../test.rb:2: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): v16:HeapBasicObject = GuardType v6, HeapBasicObject v17:CShape = LoadField v16, :shape_id@0x4 v19:StringExact[VALUE(0x103ba9440)] = Const Value(VALUE(0x103ba9440)) CheckInterrupts Return v19
  6. 1 Side Exit JIT でハンドリングできない場合※ に、VM に処理を戻す 他の処理系だとdeopt(de-optimization) とか言ったりします ※

    そもそもJIT でハンドリングできない理由はまちまち (プロファイルとは違う条件での実行、未対応のバイトコード、メソッド再定義、割り込み... )
  7. 1 当然のようにSide Exit はやりたくない JIT 側での処理を増やせば増やすほど早くなる(逆も然り) The first rule of

    just-in-time compilers is: you stay in JIT code. The second rule of JIT is: you STAY in JIT code! ref: https://railsatscale.com/2026-03-27-using-perfetto-in-zjit/
  8. 1 プロファイルを新しく取り直して、Compile をやり直す 1. Side Exit するたびにプロファイルをとる 2. 古いISEQ を無効化し、ISEQ

    単位でCompile をやり直す これにより、プロファイルがないケースや、漏れているケースのカバーができるようになる ※ もうちょい細かく分岐があるんですが、ざっくり書くとこんな感じです
  9. 1 具体的な例で考えてみる def test = defined?(@a) class C end obj

    = C.new obj.instance_variable_set(:@a, 1) 50.times { obj.test } # @a 存在化でプロファイル v18:CShape[0x80008] = GuardBitEquals v17, CShape(0x80008) v19:StringExact[VALUE(0x1017194d0)] = Const Value(VALUE(0x1017194d0)) CheckInterrupts bb1(): EntryPoint interpreter v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:HeapBasicObject): v17:CShape = LoadField v6, :shape_id@0x4 Return v19 このような単一プロファイルしかないパスをMonomorphic と言ったりします
  10. 1 具体的な例で考えてみる2 obj = C.new obj.instance_variable_set(:@b, 1) # @b しか持たないインスタンス

    50.times { obj.test } # b はcompile されたJIT コードに存在しないので、Side Exit...!! class C def test = defined?(@a) end obj = C.new obj.instance_variable_set(:@a, 1) 50.times { obj.test } # @a 存在化でプロファイル v18:CShape[0x80008] = GuardBitEquals v17, CShape(0x80008) # ここでSide Exit する bb3(v6:HeapBasicObject): v17:CShape = LoadField v6, :shape_id@0x4 v19:StringExact[VALUE(0x1017194d0)] = Const Value(VALUE(0x1017194d0)) CheckInterrupts Return v19
  11. 1 具体的な例で考えてみる3 class C def test = defined?(@a) end obj

    = C.new obj.instance_variable_set(:@a, 1) 50.times { obj.test } obj = C.new obj.instance_variable_set(:@b, 1) 50.times { obj.test } bb4(v11:StringExact|NilClass): CheckInterrupts Return v11 bb3(v6:HeapBasicObject): v12:CShape = LoadField v6, :shape_id@0x4 v13:CShape[0x80009] = Const CShape(0x80009) v14:CBool = IsBitEqual v12, v13 CondBranch v14, bb5(), bb6() bb5(): v16:NilClass = Const Value(nil) # @b だけのパス Jump bb4(v16) bb6(): v18:CShape = LoadField v6, :shape_id@0x4 v19:CShape[0x80008] = Const CShape(0x80008) v20:CBool = IsBitEqual v18, v19 CondBranch v20, bb7(), bb8() bb7(): v22:StringExact[VALUE(0x1017194d0)] = Const Value(VALUE(0x1017194d0)) Jump bb4(v22) bb8(): v24:StringExact|NilClass = DefinedIvar v6, :@a # Fallback の汎用パス Jump bb4(v24) ※ 生成されるHIR は観測回数に依存して並べられ、今回は同数で新しいものが前になる形