Share
Embed
BYOJ: Build your Own JIT (RubyConfTH 2023)
Syed Faraaz Ahmad
October 06, 2023
Transcript
64 slides
-
What is a compiler? Ahead-of-Time compiler Just-in-Time compiler C, C++,
Rust, etc Ruby, Python, Java, etc Compile, Then Execute Compile While Executing -
The many JITs of Ruby MJIT YJIT First ever JIT
for Ruby. Introduced in Ruby 2.6 To be discontinued RJIT New first-class JIT released with Ruby 3.0 Default JIT written in Ruby, shipping with Ruby 3.3 Experimental only -
The many JITs of Ruby MJIT YJIT First ever JIT
for Ruby. Introduced in Ruby 2.6 To be discontinued RJIT New first-class JIT released with Ruby 3.0 Default JIT written in Ruby, shipping with Ruby 3.3 Experimental only -
Physical Machine Virtual Machine CPU Virtual CPU Core i9, Apple
M2 Ruby interpreter (YARV) -
YARV (Yet Another Ruby VM) A Stack machine: Stores/retrieves all
your objects onto/from the stack 1 + 2 SP push 1 push 2 add PC PC: Program counter SP: Stack pointer -
YARV (Yet Another Ruby VM) A Stack machine: Stores/retrieves all
your objects onto/from the stack 1 SP 1 + 2 push 1 push 2 add PC -
YARV (Yet Another Ruby VM) A Stack machine: Stores/retrieves all
your objects onto/from the stack 1 2 SP 1 + 2 push 1 push 2 add PC -
YARV (Yet Another Ruby VM) A Stack machine: Stores/retrieves all
your objects onto/from the stack SP 1 + 2 push 1 push 2 add PC 2, 1 -
YARV (Yet Another Ruby VM) A Stack machine: Stores/retrieves all
your objects onto/from the stack 3 SP 1 + 2 push 1 push 2 add PC -
CPU A Register machine: Stores/retrieves all your values onto/from registers
1 + 2 mov r1, 1 mov r2, 2 add r1, r2 PC Register Value r1 1 r2 r3 -
CPU A Register machine: Stores/retrieves all your values onto/from registers
1 + 2 mov r1, 1 mov r2, 2 add r1, r2 PC Register Value r1 1 r2 2 r3 -
CPU A Register machine: Stores/retrieves all your values onto/from registers
1 + 2 mov r1, 1 mov r2, 2 add r1, r2 PC Register Value r1 3 r2 2 r3 -
Compiling nil 1. Put nil onto the stack 2. return
nil SP def foo nil end -
Compiling nil 1. Put nil onto the stack 2. return
nil nil SP def foo nil end -
Compiling nil 1. Put nil onto the stack 2. return
nil nil SP WHERE DOES THE nil GO?? def foo nil end -
Instruction sequence $ ruby --dump=insns foo.rb Looking under the hood
== disasm: #<ISeq:<main>foo.rb:1 (1,0)-(3,3)> 0000 definemethod :foo, foo 0003 putobject :foo 0005 leave == disasm: #<ISeq:[email protected]:1 (1,0)-(3,3)> 0000 putnil 0001 leave -
nil SP WHERE DO WE leave FROM?? WHERE DO WE
leave TO?? 0000 putnil 0001 leave -
Stack traces • Shows the exact path from which you
errored out • Traces it all the way to the top level: <main> def foo raise StandardError end def bar foo end bar frames.rb:2:in `foo': StandardError from frames.rb:6:in `bar' from frames.rb:9:in `<main>' -
A data structure that knows the following: frames.rb:2:in `foo': StandardError
from frames.rb:6:in `bar' from frames.rb:9:in `<main>' 1. Which method you are in right now -
A data structure that knows the following: frames.rb:2:in `foo': StandardError
from frames.rb:6:in `bar' from frames.rb:9:in `<main>' 1. Which method you are in right now 2. Where in the program you are in right now -
A data structure that knows the following: frames.rb:2:in `foo': StandardError
from frames.rb:6:in `bar' from frames.rb:9:in `<main>' 1. Which method you are in right now 2. Where in the program you are in right now 3. What value you need to return -
A data structure that knows the following: frames.rb:2:in `foo': StandardError
from frames.rb:6:in `bar' from frames.rb:9:in `<main>' 1. Which method you are in right now 2. Where in the program you are in right now 3. What value you need to return -
A data structure that knows the following: frames.rb:2:in `foo': StandardError
from frames.rb:6:in `bar' from frames.rb:9:in `<main>' 1. Which method you are in right now 2. Where in the program you are in right now 3. What value you need to return -
A data structure that knows the following: frames.rb:2:in `foo': StandardError
from frames.rb:6:in `bar' from frames.rb:9:in `<main>' 1. Which method you are in right now 2. Where in the program you are in right now 3. What value you need to return -
A data structure that knows the following: frames.rb:2:in `foo': StandardError
from frames.rb:6:in `bar' from frames.rb:9:in `<main>' 1. Which method you are in right now 2. Where in the program you are in right now 3. What value you need to return 1. Which method you are in right now 2. Where in the program you are in right now 3. What value you need to return 1. Which method you are in right now 2. Where in the program you are in right now 3. What value you need to return Needs to be stacked: 1. So Every method has its own copy of context 2. Can be unrolled when a complete backtrace needs to be shown -
Control frames rb_control_frame_t [TOP] rb_control_frame_t [EVAL] rb_control_frame_t [METHOD:bar] CFP rb_control_frame_t
[METHOD:foo] Represent the path taken by your program pc sp self ... rb_control_frame_t Program counter Stack pointer frames.rb:2:in `foo': StandardError from frames.rb:6:in `bar' from frames.rb:9:in `<main>' -
Increment CFP to leave == disasm: #<ISeq:<main>@main.rb:1 (1,0)-(1,3)> (catch: FALSE)
0000 putnil 0001 leave Increment Control Frame Pointer -
Using RJIT # Replace RJIT with JIT::Compiler RubyVM::RJIT::Compiler.prepend(Module.new { def
compile(iseq, _) @compiler ||= JIT::Compiler.new @compiler.compile(iseq) end }) # Enable JIT compilation (paused by --rjit=pause) RubyVM::RJIT.resume -
Building our own JIT # Replace RJIT with JIT::Compiler RubyVM::RJIT::Compiler.prepend(Module.new
{ def compile(iseq, _) @compiler ||= JIT::Compiler.new @compiler.compile(iseq) end }) # Enable JIT compilation (paused by --rjit=pause) RubyVM::RJIT.resume We write the JIT::Compiler class -
The JIT::Compiler class module JIT class Compiler # Utilities to
call C functions and interact with the Ruby VM. C = RubyVM::RJIT::C # Metadata for each YARV instruction. INSNS = RubyVM::RJIT::INSNS # Compile a method def compile(iseq) # Write machine code to this assembler. asm = Assembler.new # Iterate over each YARV instruction. insn_index = 0 while insn_index < iseq.body.iseq_size # Compile our instructions here end end end end -
The JIT::Compiler class module JIT class Compiler # Utilities to
call C functions and interact with the Ruby VM. C = RubyVM::RJIT::C # Metadata for each YARV instruction. INSNS = RubyVM::RJIT::INSNS # Compile a method def compile(iseq) # Write machine code to this assembler. asm = Assembler.new # Iterate over each YARV instruction. insn_index = 0 while insn_index < iseq.body.iseq_size # Compile our instructions here end end end end # Utilities to call C functions and interact with the Ruby VM. C = RubyVM::RJIT::C # Metadata for each YARV instruction. INSNS = RubyVM::RJIT::INSNS -
The JIT::Compiler class module JIT class Compiler # Utilities to
call C functions and interact with the Ruby VM. C = RubyVM::RJIT::C # Metadata for each YARV instruction. INSNS = RubyVM::RJIT::INSNS # Compile a method def compile(iseq) # Write machine code to this assembler. asm = Assembler.new # Iterate over each YARV instruction. insn_index = 0 while insn_index < iseq.body.iseq_size # Compile our instructions here end end end end def compile(iseq) # Write machine code to this assembler. asm = Assembler.new # Iterate over each YARV instruction. insn_index = 0 while insn_index < iseq.body.iseq_size # Compile our instructions here end end -
Compiling putnil # Iterate over each YARV instruction. insn_index =
0 while insn_index < iseq.body.iseq_size insn = INSNS.fetch( C.rb_vm_insn_decode(iseq.body.iseq_encoded[insn_index]) ) case insn.name in :putnil # ... end insn_index += insn.len end -
Compiling putnil STACK = [:r8, :r9] stack_size = 0 while
insn_index < iseq.body.iseq_size # ... case insn.name # ... in :putnil asm.mov(STACK[stack_size], C.to_value(nil)) stack_size += 1 end end :r8 :r9 SP nil r8 register -
Compiling leave EC = :rdi CFP = :rsi while insn_index
< iseq.body.iseq_size case insn.name # ... in :leave asm.add(CFP, C.rb_control_frame_t.size) asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], CFP) end end -
Compiling leave asm.add( CFP, C.rb_control_frame_t.size ) rb_control_frame_t [TOP] rb_control_frame_t [EVAL]
rb_control_frame_t [METHOD:bar] CFP 0000 0007 0008 0015 0016 0024 -
Compiling leave rb_control_frame_t [TOP] rb_control_frame_t [EVAL] rb_control_frame_t [METHOD:bar] CFP 0000
0007 0008 0015 0016 0024 “Pop a control frame from the stack” asm.add( CFP, C.rb_control_frame_t.size ) -
Compiling leave asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], CFP) rb_control_frame_t [TOP] rb_control_frame_t [EVAL] rb_control_frame_t
[METHOD:bar] CFP pc sp self ... rb_control_frame_t -
Compiling leave rb_control_frame_t [TOP] rb_control_frame_t [EVAL] rb_control_frame_t [METHOD:bar] CFP “Set
execution context to current control frame” pc sp self ... rb_control_frame_t asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], CFP) -
Compiling leave EC = :rdi CFP = :rsi while insn_index
< iseq.body.iseq_size case insn.name # ... in :leave asm.add(CFP, C.rb_control_frame_t.size) asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], CFP) asm.mov(:rax, STACK[stack_size - 1]) asm.ret end end -
Compiling leave EC = :rdi CFP = :rsi while insn_index
< iseq.body.iseq_size case insn.name # ... in :leave asm.add(CFP, C.rb_control_frame_t.size) asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], CFP) asm.mov(:rax, STACK[stack_size - 1]) asm.ret end end All return values go into :rax -
Compiling 2 + 3 def five 2 + 3 end
0000 putobject 2 0002 putobject 3 0004 opt_plus 0006 leave -
Compiling 2 + 3 0000 putobject 2 0002 putobject 3
0004 opt_plus 0006 leave PC SP -
Compiling 2 + 3 2 PC SP 0000 putobject 2
0002 putobject 3 0004 opt_plus 0006 leave -
Compiling 2 + 3 2 3 PC SP 0000 putobject
2 0002 putobject 3 0004 opt_plus 0006 leave -
Compiling 2 + 3 5 PC SP 0000 putobject 2
0002 putobject 3 0004 opt_plus 0006 leave -
Compiling putobject while insn_index < iseq.body.iseq_size case insn.name # ...
in :putobject operand = iseq.body.iseq_encoded[insn_index + 1] asm.mov(STACK[stack_size], operand) stack_size += 1 end end -
Compiling opt_plus while insn_index < iseq.body.iseq_size case insn.name # ...
in :opt_plus recv = STACK[stack_size - 2] obj = STACK[stack_size - 1] asm.add(recv, obj) stack_size -= 1 end end 2 3 SP :r8 :r9 -
Compiling opt_plus while insn_index < iseq.body.iseq_size case insn.name # ...
in :opt_plus recv = STACK[stack_size - 2] obj = STACK[stack_size - 1] asm.add(recv, obj) stack_size -= 1 end end 5 3 SP :r8 :r9 -
def sum 2 + 3 end puts sum ➜ ruby
plus.rb 420 -
Bonus - Monkey patching at the machine level while insn_index
< iseq.body.iseq_size case insn.name # ... in :opt_plus recv = STACK[stack_size - 2] obj = STACK[stack_size - 1] # asm.add(recv, obj) asm.mov(recv, C.to_value(420)) stack_size -= 1 end end