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

Lightning-Fast Method Calls with Ruby 4.1 ZJIT ...

Lightning-Fast Method Calls with Ruby 4.1 ZJIT / RubyKaigi 2026

Avatar for Takashi Kokubun

Takashi Kokubun

April 24, 2026

More Decks by Takashi Kokubun

Other Decks in Programming

Transcript

  1. ˒ Ruby 4.0+: ruby --zjit ˒ Experimental JIT compiler ˒

    To be productionized at Ruby 4.1 ˒ We call it “zee-jit”, not “zed-jit” ZJIT
  2. ˒ Some YJIT optimizations are not ported to ZJIT yet

    ˒ Method calls can be slower in ZJIT than in YJIT or the interpreter (for now) Why?
  3. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry Memory writes: 4 nil
  4. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler Memory writes: 5 nil
  5. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags Memory writes: 6 nil
  6. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc Memory writes: 7 nil
  7. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc Memory writes: 8 sp nil
  8. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc Memory writes: 9 sp iseq nil
  9. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc Memory writes: 10 sp iseq self nil
  10. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc Memory writes: 11 sp iseq self ep nil
  11. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc Memory writes: 12 sp iseq self ep block code nil
  12. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc Memory writes: 13 sp iseq self ep block code jit return nil
  13. Ruby VM stack Interpreter 
 Stack: six 3 <main> Locals:

    add1 2 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc Memory writes: 14 sp iseq self ep block code jit return Ruby thread rb_execution_context_t vm_stack ... cfp nil
  14. ˒ Method parameters are passed in memory ˒ A lot

    of memory writes for frame fi elds ˒ This is the bo tt leneck of method calls in Ruby Method calls on the interpreter
  15. 
 Stack: six Ruby VM stack YJIT CPU Reg1 Reg2

    Reg3 3 <main> Register writes: 1
  16. 
 Stack: six Ruby VM stack YJIT CPU Reg1 Reg2

    Reg3 3 <main> 2 Register writes: 2
  17. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 1
  18. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 2 Locals: add1 nil
  19. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 3 Locals: add1 nil Env: add1 method
 entry
  20. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 4 Locals: add1 nil Env: add1 method
 entry block
 handler
  21. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 5 Locals: add1 nil Env: add1 method
 entry block
 handler flags
  22. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 6 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 sp
  23. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 7 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 sp iseq
  24. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 8 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 sp iseq self
  25. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 9 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 sp iseq self ep
  26. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 10 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 sp iseq self ep block code
  27. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 11 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 sp iseq self ep block code jit return
  28. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 12 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 sp iseq self ep block code jit return Ruby thread rb_execution_context_t vm_stack ... cfp
  29. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 12 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 sp iseq self ep block code jit return Ruby thread rb_execution_context_t vm_stack ... cfp = Memory writes: 14 - 1 Interpreter - PC
  30. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 12 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc sp iseq self ep block code jit return Ruby thread rb_execution_context_t vm_stack ... cfp = + Register Allocation Register writes: 2 Memory writes: -2 Memory writes: 14 - 1 Interpreter - PC
  31. Ruby VM stack 
 Stack: six 3 YJIT CPU Reg1

    Reg2 Reg3 3 <main> 2 Register writes: 2 Memory writes: 12 Locals: add1 nil Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 pc sp iseq self ep block code jit return Ruby thread rb_execution_context_t vm_stack ... cfp = + Register Spill Memory writes: 1 + Register Allocation Register writes: 2 Memory writes: -2 Memory writes: 14 - 1 Interpreter - PC
  32. ˒ Method parameters are passed in registers ˒ Most writes

    into frame fi elds (except PC) are not optimized ˒ This is still the bo tt leneck of method calls in Ruby Method calls in YJIT
  33. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 1 3 Arg2 Arg1
  34. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 1 3 Arg2 Arg1 Locals: add1
  35. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 2 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry
  36. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 3 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler
  37. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 4 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags
  38. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 5 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 iseq
  39. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 6 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 iseq self
  40. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 7 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 iseq self ep
  41. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT Memory writes: 8 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 iseq self ep block code
  42. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 iseq self ep block code <main> Register writes: 1 Memory writes: 8
  43. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 iseq self ep block code <main> Register writes: 2 Memory writes: 8 2
  44. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 iseq self ep block code <main> Register writes: 2 Memory writes: 9 2 C stack Stack: six 3
  45. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 ZJIT 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler flags rb_control_frame_t: add1 iseq self ep block code <main> Register writes: 2 Memory writes: 10 2 C stack Stack: six 3 Ruby thread rb_execution_context_t vm_stack ... cfp
  46. Interpreter vs YJIT vs ZJIT Interpreter YJIT ZJIT Memory writes

    14 12 10 Register writes 0 2 2 Caller stack size: 1
  47. Interpreter vs YJIT vs ZJIT Interpreter YJIT ZJIT Memory writes

    14 15 16 Register writes 0 2 2 Caller stack size: 4
  48. ˒ Method parameters are passed in C ABI registers ˒

    Most writes into frame fi elds (except PC and jit_return) are not optimized ˒ ZJIT spills registers into both the VM stack and the C stack Method calls in ZJIT
  49. ˒ Backtraces need to reconstruct the full call chain at

    any point ˒ Even if most calls never raise, the metadata must always be there Backtraces
  50. ˒ The interpreter may suddenly take over JIT-compiled stack slots

    ˒ Frame layout must stay compatible with interpreter expectations Exception handling
  51. ˒ Debuggers can access local variables outside the current frame

    ˒ Ruby must keep locals in a predictable location on the stack Local variables
  52. ˒ Lightweight Frames reduce frame push to writing a single

    fi eld ˒ Everything still works: backtraces, exceptions, debugger access Writing only one fi eld on a method call
  53. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> ZJIT: Lightweight Frames 3 Arg2 Arg1
  54. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 Arg2 Arg1 Locals: add1 ZJIT: Lightweight Frames
  55. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 Arg2 Arg1 Locals: add1 Env: add1 ZJIT: Lightweight Frames
  56. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 Memory writes: 1 Arg2 Arg1 Locals: add1 Env: add1 rb_control_frame_t: add1 ZJIT: Lightweight Frames jit return struct zjit_jit_frame method
 entry block
 handler iseq self ep block code JIT Frame pc sp flags
  57. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 Arg2 Arg1 Locals: add1 Env: add1 rb_control_frame_t: add1 ZJIT: Lightweight Frames jit return struct zjit_jit_frame method
 entry block
 handler iseq self ep block code JIT Frame pc sp flags Memory writes: 2 C stack Stack: six 3
  58. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 Arg2 Arg1 Locals: add1 Env: add1 rb_control_frame_t: add1 ZJIT: Lightweight Frames jit return struct zjit_jit_frame method
 entry block
 handler iseq self ep block code JIT Frame pc sp flags Register writes: 1 Memory writes: 2 C stack Stack: six 3 <main>
  59. 
 Stack: six Ruby VM stack CPU Reg1 Reg2 Reg3

    <main> 3 Arg2 Arg1 Locals: add1 Env: add1 rb_control_frame_t: add1 ZJIT: Lightweight Frames jit return struct zjit_jit_frame method
 entry block
 handler iseq self ep block code JIT Frame pc sp flags Register writes: 2 Memory writes: 2 C stack Stack: six 3 <main> 2
  60. struct zjit_jit_frame 
 Stack: six Ruby VM stack CPU Reg1

    Reg2 Reg3 <main> 3 Arg2 Arg1 Locals: add1 Env: add1 method
 entry block
 handler rb_control_frame_t: add1 iseq self ep block code <main> 2 C stack Stack: six 3 Ruby thread rb_execution_context_t vm_stack ... cfp Register writes: 2 Memory writes: 3 JIT Frame pc sp flags jit return ZJIT: Lightweight Frames
  61. Interpreter vs YJIT vs ZJIT Interpreter YJIT ZJIT before ZJIT

    after Memory writes 14 12 10 3 Register writes 0 2 2 2
  62. ˒ Materializing JIT frame is slow; we want to avoid

    writing them ˒ The register allocator needs to spill registers into locations that the interpreter can retrieve as needed ˒ For local variables, and longjmp for exceptions Challenges in Lightweight Frames
  63. ˒ PC, ISEQ, and block_code are already optimized away ˒

    They are mostly queried, not materialized in many cases ˒ Future work: ˒ SP, EP, self ˒ Env: method entry, block handler, fl ags ˒ Spills into the VM stack Lightweight Frames: Current State
  64. ˒ Method inlining can use Lightweight Frames to save contexts

    for pro fi lers and other features ˒ Lightweight Frames does not remove register spills into the C stack, but method inlining can Method inlining with Lightweight Frames
  65. Conclusion ˒ Ruby’s method calls are slow because it needs

    to write many fi elds that are read out-of-frame ˒ ZJIT’s Lightweight Frames will make method calls faster by lazily writing metadata
  66. Let’s make Ruby faster together ˒ Anybody can understand and

    develop ZJIT by asking AI today ˒ Chat with us on Zulip: h tt ps://zjit.zulipchat.com/ ˒ See also: h tt ps://github.com/Shopify/ruby/issues