Save 37% off PRO during our Black Friday Sale! »

Some Assembly Required

Some Assembly Required

My RubyConf 2021 presentation

F29327647a9cff5c69618bae420792ea?s=128

Aaron Patterson

November 13, 2021
Tweet

Transcript

  1. None
  2. Aaron Patterson Some Assembly Required ASSEMBLY! GET IT???? LIKE, YOU

    HAVE TO PUT IT TOGETHER BUT ALSO IT USES ASSEMBLY LANGUAGE???
  3. Aaron Patterson

  4. None
  5. None
  6. @tenderlove

  7. None
  8. Ruby Core

  9. Rails Core

  10. I AM SO HAPPY TO BE HERE

  11. I LIKE TO SEE PEOPLE IN 3D

  12. I love local stuff!

  13. None
  14. Chili Colorado

  15. https://www.bonappetit.com/recipes/article/groat-ricks-chili-colorado It’s a traditional Mexican dish of beef or pork

    stewed in a red chili sauce—chili “colored red,” not chili from the state of Colorado.
  16. https://github.com/tenderlove/tenderjit

  17. A JIT for Ruby, Written in Ruby

  18. https://github.com/chrisseaton/rhizome

  19. Ludicrous JIT Compiler https://github.com/cout/ludicrous

  20. TenderJIT’s Goals

  21. 1. Be Fun ✅

  22. 2. Be Written in Pure Ruby ✅

  23. 3. Installable as a Gem 🚫

  24. 4. Speed Stuff Up? 🤷

  25. Why TenderJIT?

  26. A Learning Tool

  27. YJIT

  28. To See If I Could

  29. Does it Work?

  30. Fibonacci Benchmark YARV vs TenderJIT require "benchmark" def fib(n) if

    n < 2 n else fib(n - 2) + fib(n - 1) end end N = 40 puts BASE: Benchmark.measure { fib(N) }.total if ARGV[0] == "TJ" require "tenderjit" jit = TenderJIT.new jit.compile(method(:fib)) jit.enable! puts TJ1: Benchmark.measure { fib(N) }.total # Warm puts TJ2: Benchmark.measure { fib(N) }.total jit.disable! end Doesn’t Automatically Compile Stuff (yet) $ be ruby -I lib:test fib.rb TJ {:BASE=>9.943876999999999} {:TJ1=>3.9653509999999996} {:TJ2=>3.8689980000000004}
  31. Fibonacci Benchmark YJIT require "benchmark" def fib(n) if n <

    2 n else fib(n - 2) + fib(n - 1) end end N = 40 puts BASE: Benchmark.measure { fib(N) }.total if ARGV[0] == "TJ" require "tenderjit" jit = TenderJIT.new jit.compile(method(:fib)) jit.enable! puts TJ1: Benchmark.measure { fib(N) }.total # Warm puts TJ2: Benchmark.measure { fib(N) }.total jit.disable! end $ be ruby --yjit -I lib:test fib.rb {:BASE=>2.2541659999999997}
  32. 😅

  33. Building a JIT from Nothing

  34. Build Your Own TenderJIT!

  35. What is a JIT?

  36. What is “Machine Code”?

  37. Machine Code is a Sequence of Bytes

  38. SQL

  39. PostScript

  40. Generate Machine Code print "H\xC7\xC0*\x00\x00\x00\xC3" $ ruby machine2.rb | ndisasm

    -b 64 - 00000000 48C7C02A000000 mov rax,0x2a 00000007 C3 ret Ruby Program Disassembled Output
  41. Template Compiler

  42. ERB Template <h1>Machine Code is Cool!</h1> <ul> <li> Move <%=

    value_1 %> to register R9 </li> <li> Move <%= value_2 %> to register RAX </li> <li> Add RAX and R9 </li> <ul>
  43. Let’s Build a JIT!!

  44. Legit JIT

  45. Legit JIT stored in Git

  46. 2 LeJIT in Git (hey hey!)

  47. Building a JIT: Two Things

  48. A Way To Generate Machine Code

  49. Translate Ruby To Machine Code

  50. YARV vs CPU

  51. YARV is a Stack Machine

  52. Stack Machine 5 + 3 push 5 push 3 plus

    Source Code Machine Code 5 3 8 Machine Stack
  53. Stack Machine 5 + 3 push 5 push 3 plus

    Source Code Machine Code 5 3 Machine Stack PC (Program Counter) SP (Stack Pointer)
  54. YARV Instructions $ cat thing.rb 5 + 3 $ ruby

    --dump=insns thing.rb == disasm: #<ISeq:<main>@thing.rb:1 (1,0)-(1,5)> (catch: FALSE) 0000 putobject 5 ( 1)[Li] 0002 putobject 3 0004 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] 0006 leave
  55. Infinite Stack Depth! (not really in fi nite, but ykwim)

    $ cat thing.rb foo(:foo, :bar, :baz, :zot, :hoge, :hoge2) $ ruby --dump=insns thing.rb == disasm: #<ISeq:<main>@thing.rb:1 (1,0)-(1,42)> (catch: FALSE) 0000 putself ( 1)[Li] 0001 putobject :foo 0003 putobject :bar 0005 putobject :baz 0007 putobject :zot 0009 putobject :hoge 0011 putobject :hoge2 0013 opt_send_without_block <calldata!mid:foo, argc:6, FCALL|ARGS_SIMPLE> Stack is now 7 deep!!
  56. CPU is a Register Machine

  57. Register Machine 5 + 3 mov r1, 5 mov r2,

    3 add r1, r2 Source Code Machine Code Machine Registers Register Name Value r1 r2 r3 … 5 3 8
  58. x86 Instructions int main(int argc, char *argv[]) { int x

    = 5; int y = 3; return x + y; } 100003fa2: mov dword ptr [rbp - 20], 5 100003fa9: mov dword ptr [rbp - 24], 3 100003fb0: mov eax, dword ptr [rbp - 20] 100003fb3: add eax, dword ptr [rbp - 24] Source Code Machine Code
  59. x86 Instructions int main(int argc, char *argv[]) { int x

    = 5; int y = 3; return x + y; } 100003fa2: mov dword ptr [rbp - 20], 5 100003fa9: mov dword ptr [rbp - 24], 3 100003fb0: mov eax, dword ptr [rbp - 20] 100003fb3: add eax, dword ptr [rbp - 24] Source Code Machine Code
  60. x86 Instructions int main(int argc, char *argv[]) { int x

    = 5; int y = 3; return x + y; } 100003fa2: mov dword ptr [rbp - 20], 5 100003fa9: mov dword ptr [rbp - 24], 3 100003fb0: mov eax, dword ptr [rbp - 20] 100003fb3: add eax, dword ptr [rbp - 24] Source Code Machine Code
  61. x86 Instructions int main(int argc, char *argv[]) { int x

    = 5; int y = 3; return x + y; } 100003fa2: mov dword ptr [rbp - 20], 5 100003fa9: mov dword ptr [rbp - 24], 3 100003fb0: mov eax, dword ptr [rbp - 20] 100003fb3: add eax, dword ptr [rbp - 24] Source Code Machine Code Register “EAX”
  62. x86 Instructions int main(int argc, char *argv[]) { int x

    = 5; int y = 3; return x + y; } 100003fa2: mov dword ptr [rbp - 20], 5 100003fa9: mov dword ptr [rbp - 24], 3 100003fb0: mov eax, dword ptr [rbp - 20] 100003fb3: add eax, dword ptr [rbp - 24] Source Code Machine Code
  63. Generate Machine Code

  64. Fisk: x86-64 Assembler https://github.com/tenderlove/ fi sk

  65. Fisk Example Add 5 and 3 require "fisk" fisk =

    Fisk.new # Write 5 to R9 fisk.mov(fisk.r9, fisk.imm(5)) # Write 3 to RAX fisk.mov(fisk.rax, fisk.imm(3)) # Add R9 and RAX fisk.add(fisk.rax, fisk.r9) # Return fisk.ret fisk.write_to($stdout) $ ruby machine.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 48C7C003000000 mov rax,0x3 0000000E 4C01C8 add rax,r9 00000011 C3 ret
  66. Fisk Example Eval Based API require "fisk" fisk = Fisk.new

    fisk.asm do # Write 5 to R9 mov(r9, imm(5)) # Write 3 to RAX mov(rax, imm(3)) # Add R9 and RAX add(rax, r9) # Return ret end fisk.write_to($stdout) $ ruby machine.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 48C7C003000000 mov rax,0x3 0000000E 4C01C8 add rax,r9 00000011 C3 ret
  67. Run the Bytes! 🏃

  68. Run Some Bytes 🏃🏃🏃 fisk = Fisk.new fisk.asm do #

    Write "5" in to the R9 register mov(r9, imm(0x5)) # Write "3" in to the RAX register mov(rax, imm(0x3)) # Add RAX and R9. Result will end up in RAX add(rax, r9) # Return from this function ret end # Allocate some executable memory jit_buffer = Fisk::Helpers.jitbuffer 4096 # Assemble the code and write to the JIT buffer fisk.write_to jit_buffer p jit_buffer.to_function([], Fiddle::TYPE_INT).call $ ruby add_two_numbers.rb 8 Code Output Write our code Allocate executable memory Write the bytes to executable memory Point the CPU at our bytes!
  69. Just Define A Method! # [snip] # Allocate some executable

    memory jit_buffer = Fisk::Helpers.jitbuffer 4096 # Assemble the code and write to the JIT buffer fisk.write_to jit_buffer func = jit_buffer.to_function([], Fiddle::TYPE_INT) define_method :add, &func p add De fi ne a method!
  70. Did we do a JIT?

  71. We did a JIT!

  72. In Pure Ruby!

  73. 😅😅😅😅😅😅 fisk = Fisk.new fisk.asm do # Copy first parameter

    to RAX mov(rax, rdi) # Add second parameter to RAX add(rax, rsi) # Return from this function ret end “Ruby”
  74. What We’d Really Like Automatic Conversion def add 5 +

    3 end fisk = Fisk.new fisk.asm do # Copy 5 to RAX mov(rax, imm(5)) # Copy 3 to RSI mov(rsi, imm(3)) # Add RAX and RSI add(rax, rsi) # Return from this function ret end 🤔
  75. YARV ➡ x86 Converter

  76. Dump YARV Instructions ruby —dump=insns test.rb def add 5 +

    3 end $ ruby --dump=insns test.rb == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(3,3)> (catch: FALSE) 0000 definemethod :add, add ( 1)[Li] 0003 putobject :add 0005 leave == disasm: #<ISeq:add@test.rb:1 (1,0)-(3,3)> (catch: FALSE) 0000 putobject 5 ( 2)[LiCa] 0002 putobject 3 0004 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] 0006 leave ( 3)[Re]
  77. RubyVM::InstructionSequence

  78. Access Instruction From Ruby def add 5 + 3 end

    iseq = RubyVM::InstructionSequence.of(method(:add)) pp iseq.to_a $ ruby -rpp test.rb ["YARVInstructionSequence/SimpleDataFormat", 3, 1, 1, {:arg_size=>0, :local_size=>0, :stack_max=>2, :node_id=>7, :code_location=>[1, 0, 3, 3], :node_ids=>[3, 4, 6, -1]}, "add", "test.rb", "/Users/aaron/git/presentations/2021/RubyConf/test.rb", 1, :method, [], {}, [], [2, :RUBY_EVENT_LINE, :RUBY_EVENT_CALL, [:putobject, 5], [:putobject, 3], [:opt_plus, {:mid=>:+, :flag=>16, :orig_argc=>1}], 3, :RUBY_EVENT_RETURN, [:leave]]]
  79. Access Instruction From Ruby def add 5 + 3 end

    iseq = RubyVM::InstructionSequence.of(method(:add)) pp iseq.to_a $ ruby -rpp test.rb ["YARVInstructionSequence/SimpleDataFormat", 3, 1, 1, {:arg_size=>0, :local_size=>0, :stack_max=>2, :node_id=>7, :code_location=>[1, 0, 3, 3], :node_ids=>[3, 4, 6, -1]}, "add", "test.rb", "/Users/aaron/git/presentations/2021/RubyConf/test.rb", 1, :method, [], {}, [], [2, :RUBY_EVENT_LINE, :RUBY_EVENT_CALL, [:putobject, 5], [:putobject, 3], [:opt_plus, {:mid=>:+, :flag=>16, :orig_argc=>1}], 3, :RUBY_EVENT_RETURN, [:leave]]]
  80. Mini YARV def add 5 + 3 end stack =

    [] iseq = RubyVM::InstructionSequence.of(method(:add)) instructions = iseq.to_a.last instructions.each do |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] p PUSH: v stack << v in [:opt_plus, v] left = stack.shift right = stack.shift p POP: [left, right] p PUSH: left + right stack << (left + right) in [:leave] p RETURN: stack.shift end end $ ruby test.rb {:PUSH=>5} {:PUSH=>3} {:POP=>[5, 3]} {:PUSH=>8} {:RETURN=>8} Stack Object!
  81. Stack vs Registers

  82. “Virtual” Stack

  83. Virtual Stack class Stack include Fisk::Registers REGISTERS = [R9, R10]

    def initialize @depth = 0 end # Push on the stack. Returns the location # to write a temporary variable def push reg = REGISTERS.fetch(@depth) @depth += 1 reg end # Pop off the stack. Returns the location # to read the temporary variable def pop @depth -= 1 REGISTERS.fetch(@depth) end end stack = Stack.new location = stack.push write(location, temporary_value) Max Depth: 2
  84. Integrate Virtual Stack stack = Stack.new instructions.each do |insn| next

    unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push write(location, v) # Write the value in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right result = add(left, right) # Get the location to push location = stack.push write(location, result) in [:leave] result = stack.pop # Write the result to the return location write(return_location, result) # Then return ret end end Where should I write? Where should I read? Where should I write? Where should I read?
  85. Add Fisk (for x86 code) stack = Stack.new fisk =

    Fisk.new instructions.each do |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push # Write the value fisk.mov(location, fisk.imm(v)) in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right fisk.add(left, right) # Get the location to push location = stack.push fisk.mov(location, left) in [:leave] result = stack.pop # Write the result to the return location fisk.mov(fisk.rax, result) # Then return fisk.ret end end fisk.write_to($stdout) Code $ ruby test.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 49C7C203000000 mov r10,0x3 0000000E 4D01CA add r10,r9 00000011 4D89D1 mov r9,r10 00000014 4C89C8 mov rax,r9 00000017 C3 ret Output Write to the “stack” Add values Write to the “stack” Write to return location Print machine code
  86. Execution Example def add 5 + 3 end Code $

    ruby test.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 49C7C203000000 mov r10,0x3 0000000E 4D01CA add r10,r9 00000011 4D89D1 mov r9,r10 00000014 4C89C8 mov rax,r9 00000017 C3 ret Output Push 5 on the stack Push 5 on the stack Push 3 on the stack Push 3 on the stack Pop, Pop, Add, Push Pop, Pop, Add, Push CPU State Register Value R9 R10 RAX 5 3 8 8 8 Copy to Return Location / Leave
  87. Refactor To a Method def add 5 + 3 end

    def compile(method) iseq = RubyVM::InstructionSequence.of(method) # [snip] # Insert translation code here # [/snip] # Allocate some executable memory jit_buffer = Fisk::Helpers.jitbuffer 4096 # Assemble the code and write to the JIT buffer fisk.write_to jit_buffer # Return a lambda jit_buffer.to_function([], Fiddle::TYPE_INT) end callable = compile(method(:add)) define_method :fast_add, &callable p add => fast_add Code $ ruby test.rb {8=>8} Output
  88. Ruby => YARV => x86

  89. Optimizations

  90. Too Many Copies stack = Stack.new fisk = Fisk.new instructions.each

    do |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push # Write the value fisk.mov(location, fisk.imm(v)) in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right fisk.add(left, right) # Get the location to push location = stack.push fisk.mov(location, left) in [:leave] result = stack.pop # Write the result to the return location fisk.mov(fisk.rax, result) # Then return fisk.ret end end fisk.write_to($stdout) Code $ ruby test.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 49C7C203000000 mov r10,0x3 0000000E 4D01CA add r10,r9 00000011 4D89D1 mov r9,r10 00000014 4C89C8 mov rax,r9 00000017 C3 ret Output CPU State Register Value R9 R10 RAX 8 8 8
  91. A + B = B + A

  92. Too Many Copies stack = Stack.new fisk = Fisk.new instructions.each

    do |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push # Write the value fisk.mov(location, fisk.imm(v)) in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right fisk.add(right, left) # push stack.push in [:leave] result = stack.pop # Write the result to the return location fisk.mov(fisk.rax, result) # Then return fisk.ret end end fisk.write_to($stdout) Code $ ruby test.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 49C7C203000000 mov r10,0x3 0000000E 4D01D1 add r9,r10 00000011 4C89C8 mov rax,r9 00000014 C3 ret Output CPU State Register Value R9 R10 RAX 3 8 8
  93. Stack Depth Is 1 on Return

  94. Update Virtual Stack class Stack include Fisk::Registers # REGISTERS =

    [R9, R10] REGISTERS = [RAX, R9] def initialize @depth = 0 end # Push on the stack. Returns the location # to write a temporary variable def push reg = REGISTERS.fetch(@depth) @depth += 1 reg end # Pop off the stack. Returns the location # to read the temporary variable def pop @depth -= 1 REGISTERS.fetch(@depth) end end Last value in RAX
  95. Update Compiler stack = Stack.new fisk = Fisk.new instructions.each do

    |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push # Write the value fisk.mov(location, fisk.imm(v)) in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right fisk.add(right, left) # Get the location to push location = stack.push #fisk.mov(location, left) in [:leave] result = stack.pop # The last value is already in RAX # fisk.mov(fisk.rax, result) # Then return fisk.ret end end fisk.write_to($stdout) Code $ ruby test.rb | ndisasm -b 64 - 00000000 48C7C005000000 mov rax,0x5 00000007 49C7C103000000 mov r9,0x3 0000000E 4C01C8 add rax,r9 00000011 C3 ret Output CPU State Register Value R9 R10 RAX 8 8
  96. And Others!

  97. Did we do a JIT?

  98. Defines a New Method def add 5 + 3 end

    def compile(method) iseq = RubyVM::InstructionSequence.of(method) # [snip] # Insert translation code here # [/snip] # Allocate some executable memory jit_buffer = Fisk::Helpers.jitbuffer 4096 # Assemble the code and write to the JIT buffer fisk.write_to jit_buffer # Return a lambda jit_buffer.to_function([], Fiddle::TYPE_INT) end callable = compile(method(:add)) define_method :fast_add, &callable p add => fast_add Code
  99. How Do MJIT / YJIT Work?

  100. VM Entry Point VALUE vm_exec(rb_execution_context_t *ec, bool mjit_enable_p) { enum

    ruby_tag_type state; VALUE result = Qundef; VALUE initial = 0; EC_PUSH_TAG(ec); _tag.retval = Qnil; if ((state = EC_EXEC_TAG()) == TAG_NONE) { if (!mjit_enable_p || (result = mjit_exec(ec)) == Qundef) { result = vm_exec_core(ec, initial); } goto vm_loop_start; /* fallback to the VM */ } else { result = ec->errinfo; rb_ec_raised_reset(ec, RAISED_STACKOVERFLOW | RAISED_NOMEMORY); while ((result = vm_exec_handle_exception(ec, state, result, &initial)) == Qundef) { /* caught a jump, exec the handler */ result = vm_exec_core(ec, initial); vm_loop_start: VM_ASSERT(ec->tag == &_tag); /* when caught `throw`, `tag.state` is set. */ if ((state = _tag.state) == TAG_NONE) break; _tag.state = TAG_NONE; } } EC_POP_TAG(); return result; }
  101. VM Entry Point VALUE vm_exec(rb_execution_context_t *ec, bool mjit_enable_p) { enum

    ruby_tag_type state; VALUE result = Qundef; VALUE initial = 0; EC_PUSH_TAG(ec); _tag.retval = Qnil; if ((state = EC_EXEC_TAG()) == TAG_NONE) { if (!mjit_enable_p || (result = mjit_exec(ec)) == Qundef) { result = vm_exec_core(ec, initial); } goto vm_loop_start; /* fallback to the VM */ } else { result = ec->errinfo; rb_ec_raised_reset(ec, RAISED_STACKOVERFLOW | RAISED_NOMEMORY); while ((result = vm_exec_handle_exception(ec, state, result, &initial)) == Qundef) { /* caught a jump, exec the handler */ result = vm_exec_core(ec, initial); vm_loop_start: VM_ASSERT(ec->tag == &_tag); /* when caught `throw`, `tag.state` is set. */ if ((state = _tag.state) == TAG_NONE) break; _tag.state = TAG_NONE; } } EC_POP_TAG(); return result; }
  102. mjit_exec static inline VALUE mjit_exec(rb_execution_context_t *ec) { const rb_iseq_t *iseq

    = ec->cfp->iseq; struct rb_iseq_constant_body *body = iseq->body; bool yjit_enabled = false; #ifndef MJIT_HEADER // Don't want to compile with YJIT or use code generated by YJIT // when running inside code generated by MJIT. yjit_enabled = rb_yjit_enabled_p(); #endif if (mjit_call_p || yjit_enabled) { body->total_calls++; } #ifndef MJIT_HEADER if (yjit_enabled && !mjit_call_p && body->total_calls == rb_yjit_call_threshold()) { // If we couldn't generate any code for this iseq, then return // Qundef so the interpreter will handle the call. if (!rb_yjit_compile_iseq(iseq, ec)) { return Qundef; } } #endif if (!(mjit_call_p || yjit_enabled)) return Qundef; RB_DEBUG_COUNTER_INC(mjit_exec); mjit_func_t func = body->jit_func; // YJIT tried compiling this function once before and couldn't do // it, so return Qundef so the interpreter handles it. if (yjit_enabled && func == 0) { return Qundef; } if (UNLIKELY((uintptr_t)func <= LAST_JIT_ISEQ_FUNC)) { # ifdef MJIT_HEADER RB_DEBUG_COUNTER_INC(mjit_frame_JT2VM); # else RB_DEBUG_COUNTER_INC(mjit_frame_VM2VM); # endif return mjit_exec_slowpath(ec, iseq, body); } # ifdef MJIT_HEADER RB_DEBUG_COUNTER_INC(mjit_frame_JT2JT); # else RB_DEBUG_COUNTER_INC(mjit_frame_VM2JT); # endif RB_DEBUG_COUNTER_INC(mjit_exec_call_func); // Under SystemV x64 calling convention // ec -> RDI // cfp -> RSI return func(ec, ec->cfp); }
  103. mjit_exec (as Ruby) def mjit_exec(ec) iseq = ec.cfp.iseq body =

    iseq.body return Qundef unless jit_enabled body.total_calls += 1 if body.total_calls >= call_threshold compile_iseq(iseq) end if body.jit_func body.jit_func.call else Qundef end end
  104. JIT Contract: “Write a C function to `jit_func` and I’ll

    call it”
  105. mjit_exec (as Ruby) def mjit_exec(ec) iseq = ec.cfp.iseq body =

    iseq.body return Qundef unless jit_enabled body.total_calls += 1 if body.total_calls >= call_threshold compile_iseq(iseq) end if body.jit_func body.jit_func.call else Qundef end end
  106. EC: Execution Context There is only one! EC def recursive(n)

    return if n == 0 recursive(n - 1) end recursive(3) Sample Code
  107. CFP: Control Frame Pointer One per stack frame EC def

    recursive(n) return if n == 0 recursive(n - 1) end recursive(3) Sample Code CFP CFP
  108. ISeq: Instruction Sequence One per “executable code” EC def recursive(n)

    return if n == 0 recursive(n - 1) end recursive(3) Sample Code CFP CFP CFP CFP ISeq
  109. ISeq Body: Stuff One per ISeq EC def recursive(n) return

    if n == 0 recursive(n - 1) end recursive(3) Sample Code CFP CFP CFP CFP ISeq Body jit_func is in here!!!!
  110. RubyVM::InstructionSequence Access to ISeq object def recursive(n) return if n

    == 0 recursive(n - 1) end recursive(3) Sample Code ISeq Body m = method(:recursive) iseq = RubyVM::InstructionSequence.of(m)
  111. 😈😈😈😈😈

  112. Fiddle::Pointer: Given an address, read memory

  113. Fiddle::Pointer.new(123)[0]

  114. Fiddle.dlwrap: Returns address of Ruby object

  115. Are you sure you’re frozen? >> str = "hello".freeze =>

    "hello" >> str[0] = 'e' (irb):5:in `[]=': can't modify frozen String: "hello" (FrozenError) from (irb):5:in `<main>' from /Users/aaron/.rubies/ruby-trunk/lib/ruby/gems/3.1.0/gems/irb-1.3.8.pre.11/ exe/irb:11:in `<top (required)>' from /Users/aaron/.gem/ruby/3.1.0/bin/irb:25:in `load' from /Users/aaron/.gem/ruby/3.1.0/bin/irb:25:in `<main>' >> addr = Fiddle.dlwrap str => 4393221000 >> ptr = Fiddle::Pointer.new addr => #<Fiddle::Pointer:0x0000600000c38000 ptr=0x0000000105db3b88 size=0 free=0x0000000000000000> >> ptr[16] = 'e'.bytes.first => 101 >> str => "eello" Get the address Make a pointer Write some bytes Pro fi t
  116. Not Explaining This require "fiddle" def unfreeze str addr =

    Fiddle.dlwrap str ptr = Fiddle::Pointer.new addr flags = ptr[0, Fiddle::SIZEOF_INT].unpack1("I") flags &= ~(1 << 11) ptr[0, Fiddle::SIZEOF_INT] = [flags].pack("I") end x = "foo".freeze p x.frozen? # => true unfreeze x p x.frozen? # => false
  117. What if this were a gem? It is. Oops!

  118. How do we know where?

  119. lldb / gdb know (lldb) p *reg_cfp->iseq (const rb_iseq_t) $3

    = { flags = 0x000000000018707a wrapper = 0x0000000000000000 body = 0x00007fc2b81164b0 aux = { compile_data = NULL loader = (obj = 0x0000000000000000, index = 0) exec = { local_hooks = NULL global_trace_events = 0 } } }
  120. DWARF

  121. DWARF is just text

  122. OdinFlex: Parse DWARF data https://github.com/tenderlove/odin fl ex

  123. Read the ISeq JIT function def cool_method 1234 end m

    = method(:cool_method) # Get the ISeq Object iseq = RubyVM::InstructionSequence.of(m) # Unwrap the iseq pointer from the T_DATA iseq_ptr = RData.data(Fiddle.dlwrap(iseq)) # Read the JIT function iseq_t = RbISeqT.new(iseq_ptr) p iseq_t.body.jit_func # => 0 test.rb struct RData { /** Basic part, including flags and class. */ struct RBasic basic; RUBY_DATA_FUNC dmark; RUBY_DATA_FUNC dfree; /** Pointer to the actual C level struct that you want to wrap. */ void *data; }; Ruby Internals RData
  124. Read the ISeq JIT function def cool_method 1234 end m

    = method(:cool_method) # Get the ISeq Object iseq = RubyVM::InstructionSequence.of(m) # Unwrap the iseq pointer from the T_DATA iseq_ptr = RData.data(Fiddle.dlwrap(iseq)) # Read the JIT function iseq_t = RbISeqT.new(iseq_ptr) p iseq_t.body.jit_func # => 0 test.rb struct rb_iseq_struct { VALUE flags; /* 1 */ VALUE wrapper; /* 2 */ struct rb_iseq_constant_body *body; /* 3 */ union { /* 4, 5 words */ struct iseq_compile_data *compile_data; /* used at compile time */ struct { VALUE obj; int index; } loader; struct { struct rb_hook_list_struct *local_hooks; rb_event_flag_t global_trace_events; } exec; } aux; }; Ruby Internals
  125. Read the ISeq JIT function def cool_method 1234 end m

    = method(:cool_method) # Get the ISeq Object iseq = RubyVM::InstructionSequence.of(m) # Unwrap the iseq pointer from the T_DATA iseq_ptr = RData.data(Fiddle.dlwrap(iseq)) # Read the JIT function iseq_t = RbISeqT.new(iseq_ptr) p iseq_t.body.jit_func # => 0 test.rb struct rb_iseq_constant_body { /* [SNIP] */ #if USE_MJIT /* The following fields are MJIT related info. */ VALUE (*jit_func)(struct rb_execution_context_struct *, struct rb_control_frame_struct *); /* function pointer for loaded native code */ long unsigned total_calls; /* number of total calls with `mjit_exec()` */ struct rb_mjit_unit *jit_unit; #endif }; Ruby Internals
  126. Assemble a new JIT function # Assemble a new JIT

    function fisk = Fisk.new fisk.asm do # Pop the current stack frame add(rsi, imm(RbControlFrameStruct.byte_size)) mov(m64(rdi, RbExecutionContextT.offsetof("cfp")), rsi) # Return 42 mov(rax, imm((42 << 1) | 1)) ret end buf = Fisk::Helpers.jitbuffer 1024 fisk.write_to(buf) # Assign the JIT function iseq_t.body.jit_func = buf.memory.to_i p cool_method
  127. Try Running It! $ be ruby -I lib:test thing.rb 1234

    $ be ruby --jit -I lib:test thing.rb 42 Running! def cool_method 1234 end # [snip] buf = Fisk::Helpers.jitbuffer 1024 fisk.write_to(buf) # Assign the JIT function iseq_t.body.jit_func = buf.memory.to_i p cool_method JIT Code
  128. Pure Ruby JIT: 3 Parts

  129. RubyVM::InstructionSequence

  130. x86 Translation using Fisk

  131. OdinFlex / Fiddle

  132. TenderJIT!

  133. Fisk: Low Stress x86_64 http://github.com/tenderlove/ fi sk

  134. TenderJIT: A JIT for Ruby in Ruby http://github.com/tenderlove/tenderjit

  135. YJIT: A JIT for Ruby in C https://github.com/ruby/ruby

  136. None
  137. None
  138. THANK YOU!!!!