Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

Aaron Patterson Some Assembly Required ASSEMBLY! GET IT???? LIKE, YOU HAVE TO PUT IT TOGETHER BUT ALSO IT USES ASSEMBLY LANGUAGE???

Slide 3

Slide 3 text

Aaron Patterson

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

@tenderlove

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Ruby Core

Slide 9

Slide 9 text

Rails Core

Slide 10

Slide 10 text

I AM SO HAPPY TO BE HERE

Slide 11

Slide 11 text

I LIKE TO SEE PEOPLE IN 3D

Slide 12

Slide 12 text

I love local stuff!

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

Chili Colorado

Slide 15

Slide 15 text

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.

Slide 16

Slide 16 text

https://github.com/tenderlove/tenderjit

Slide 17

Slide 17 text

A JIT for Ruby, Written in Ruby

Slide 18

Slide 18 text

https://github.com/chrisseaton/rhizome

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

TenderJIT’s Goals

Slide 21

Slide 21 text

1. Be Fun ✅

Slide 22

Slide 22 text

2. Be Written in Pure Ruby ✅

Slide 23

Slide 23 text

3. Installable as a Gem 🚫

Slide 24

Slide 24 text

4. Speed Stuff Up? 🤷

Slide 25

Slide 25 text

Why TenderJIT?

Slide 26

Slide 26 text

A Learning Tool

Slide 27

Slide 27 text

YJIT

Slide 28

Slide 28 text

To See If I Could

Slide 29

Slide 29 text

Does it Work?

Slide 30

Slide 30 text

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}

Slide 31

Slide 31 text

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}

Slide 32

Slide 32 text

😅

Slide 33

Slide 33 text

Building a JIT from Nothing

Slide 34

Slide 34 text

Build Your Own TenderJIT!

Slide 35

Slide 35 text

What is a JIT?

Slide 36

Slide 36 text

What is “Machine Code”?

Slide 37

Slide 37 text

Machine Code is a Sequence of Bytes

Slide 38

Slide 38 text

SQL

Slide 39

Slide 39 text

PostScript

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Template Compiler

Slide 42

Slide 42 text

ERB Template

Machine Code is Cool!

  • Move <%= value_1 %> to register R9
  • Move <%= value_2 %> to register RAX
  • Add RAX and R9

Slide 43

Slide 43 text

Let’s Build a JIT!!

Slide 44

Slide 44 text

Legit JIT

Slide 45

Slide 45 text

Legit JIT stored in Git

Slide 46

Slide 46 text

2 LeJIT in Git (hey hey!)

Slide 47

Slide 47 text

Building a JIT: Two Things

Slide 48

Slide 48 text

A Way To Generate Machine Code

Slide 49

Slide 49 text

Translate Ruby To Machine Code

Slide 50

Slide 50 text

YARV vs CPU

Slide 51

Slide 51 text

YARV is a Stack Machine

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Stack Machine 5 + 3 push 5 push 3 plus Source Code Machine Code 5 3 Machine Stack PC (Program Counter) SP (Stack Pointer)

Slide 54

Slide 54 text

YARV Instructions $ cat thing.rb 5 + 3 $ ruby --dump=insns thing.rb == disasm: #@thing.rb:1 (1,0)-(1,5)> (catch: FALSE) 0000 putobject 5 ( 1)[Li] 0002 putobject 3 0004 opt_plus [CcCr] 0006 leave

Slide 55

Slide 55 text

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: #@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 Stack is now 7 deep!!

Slide 56

Slide 56 text

CPU is a Register Machine

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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”

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Generate Machine Code

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Run the Bytes! 🏃

Slide 68

Slide 68 text

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!

Slide 69

Slide 69 text

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!

Slide 70

Slide 70 text

Did we do a JIT?

Slide 71

Slide 71 text

We did a JIT!

Slide 72

Slide 72 text

In Pure Ruby!

Slide 73

Slide 73 text

😅😅😅😅😅😅 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”

Slide 74

Slide 74 text

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 🤔

Slide 75

Slide 75 text

YARV ➡ x86 Converter

Slide 76

Slide 76 text

Dump YARV Instructions ruby —dump=insns test.rb def add 5 + 3 end $ ruby --dump=insns test.rb == disasm: #@test.rb:1 (1,0)-(3,3)> (catch: FALSE) 0000 definemethod :add, add ( 1)[Li] 0003 putobject :add 0005 leave == disasm: # (catch: FALSE) 0000 putobject 5 ( 2)[LiCa] 0002 putobject 3 0004 opt_plus [CcCr] 0006 leave ( 3)[Re]

Slide 77

Slide 77 text

RubyVM::InstructionSequence

Slide 78

Slide 78 text

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]]]

Slide 79

Slide 79 text

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]]]

Slide 80

Slide 80 text

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!

Slide 81

Slide 81 text

Stack vs Registers

Slide 82

Slide 82 text

“Virtual” Stack

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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?

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

Ruby => YARV => x86

Slide 89

Slide 89 text

Optimizations

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

A + B = B + A

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

Stack Depth Is 1 on Return

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

And Others!

Slide 97

Slide 97 text

Did we do a JIT?

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

How Do MJIT / YJIT Work?

Slide 100

Slide 100 text

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; }

Slide 101

Slide 101 text

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; }

Slide 102

Slide 102 text

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); }

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

JIT Contract: “Write a C function to `jit_func` and I’ll call it”

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

EC: Execution Context There is only one! EC def recursive(n) return if n == 0 recursive(n - 1) end recursive(3) Sample Code

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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!!!!

Slide 110

Slide 110 text

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)

Slide 111

Slide 111 text

😈😈😈😈😈

Slide 112

Slide 112 text

Fiddle::Pointer: Given an address, read memory

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

Fiddle.dlwrap: Returns address of Ruby object

Slide 115

Slide 115 text

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 `' from /Users/aaron/.rubies/ruby-trunk/lib/ruby/gems/3.1.0/gems/irb-1.3.8.pre.11/ exe/irb:11:in `' 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 `' >> addr = Fiddle.dlwrap str => 4393221000 >> ptr = Fiddle::Pointer.new addr => # >> ptr[16] = 'e'.bytes.first => 101 >> str => "eello" Get the address Make a pointer Write some bytes Pro fi t

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

What if this were a gem? It is. Oops!

Slide 118

Slide 118 text

How do we know where?

Slide 119

Slide 119 text

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 } } }

Slide 120

Slide 120 text

DWARF

Slide 121

Slide 121 text

DWARF is just text

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

Pure Ruby JIT: 3 Parts

Slide 129

Slide 129 text

RubyVM::InstructionSequence

Slide 130

Slide 130 text

x86 Translation using Fisk

Slide 131

Slide 131 text

OdinFlex / Fiddle

Slide 132

Slide 132 text

TenderJIT!

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

No content

Slide 137

Slide 137 text

No content

Slide 138

Slide 138 text

THANK YOU!!!!