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

Some Assembly Required

Some Assembly Required

My RubyConf 2021 presentation

Aaron Patterson

November 13, 2021
Tweet

More Decks by Aaron Patterson

Other Decks in Technology

Transcript

  1. View Slide

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

    View Slide

  3. Aaron Patterson

    View Slide

  4. View Slide

  5. View Slide

  6. @tenderlove

    View Slide

  7. View Slide

  8. Ruby Core

    View Slide

  9. Rails Core

    View Slide

  10. I AM SO HAPPY TO BE HERE

    View Slide

  11. I LIKE TO SEE PEOPLE IN 3D

    View Slide

  12. I love local stuff!

    View Slide

  13. View Slide

  14. Chili Colorado

    View Slide

  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.

    View Slide

  16. https://github.com/tenderlove/tenderjit

    View Slide

  17. A JIT for Ruby, Written in Ruby

    View Slide

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

    View Slide

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

    View Slide

  20. TenderJIT’s Goals

    View Slide

  21. 1. Be Fun ✅

    View Slide

  22. 2. Be Written in Pure Ruby ✅

    View Slide

  23. 3. Installable as a Gem 🚫

    View Slide

  24. 4. Speed Stuff Up? 🤷

    View Slide

  25. Why TenderJIT?

    View Slide

  26. A Learning Tool

    View Slide

  27. YJIT

    View Slide

  28. To See If I Could

    View Slide

  29. Does it Work?

    View Slide

  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}

    View Slide

  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}


    View Slide

  32. 😅

    View Slide

  33. Building a JIT from Nothing

    View Slide

  34. Build Your Own TenderJIT!

    View Slide

  35. What is a JIT?

    View Slide

  36. What is “Machine Code”?

    View Slide

  37. Machine Code is a


    Sequence of Bytes

    View Slide

  38. SQL

    View Slide

  39. PostScript

    View Slide

  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

    View Slide

  41. Template Compiler

    View Slide

  42. ERB Template
    Machine Code is Cool!








    Move <%= value_1 %> to register R9








    Move <%= value_2 %> to register RAX








    Add RAX and R9






    View Slide

  43. Let’s Build a JIT!!

    View Slide

  44. Legit JIT

    View Slide

  45. Legit JIT stored in Git

    View Slide

  46. 2 LeJIT in Git (hey hey!)

    View Slide

  47. Building a JIT: Two Things

    View Slide

  48. A Way To Generate


    Machine Code

    View Slide

  49. Translate Ruby To Machine Code

    View Slide

  50. YARV vs CPU

    View Slide

  51. YARV is a Stack Machine

    View Slide

  52. Stack Machine
    5 + 3 push 5


    push 3


    plus
    Source Code Machine Code
    5
    3
    8
    Machine Stack

    View Slide

  53. Stack Machine
    5 + 3 push 5


    push 3


    plus
    Source Code Machine Code
    5
    3
    Machine Stack
    PC


    (Program
    Counter) SP


    (Stack Pointer)

    View Slide

  54. 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

    View Slide

  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: #@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!!

    View Slide

  56. CPU is a Register Machine

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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”

    View Slide

  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

    View Slide

  63. Generate Machine Code

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  67. Run the Bytes! 🏃

    View Slide

  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!

    View Slide

  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!

    View Slide

  70. Did we do a JIT?

    View Slide

  71. We did a JIT!

    View Slide

  72. In Pure Ruby!

    View Slide

  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”

    View Slide

  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
    🤔

    View Slide

  75. YARV ➡ x86 Converter

    View Slide

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

    View Slide

  77. RubyVM::InstructionSequence

    View Slide

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

    View Slide

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

    View Slide

  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!

    View Slide

  81. Stack vs Registers

    View Slide

  82. “Virtual” Stack

    View Slide

  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

    View Slide

  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?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  88. Ruby => YARV => x86

    View Slide

  89. Optimizations

    View Slide

  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

    View Slide

  91. A + B = B + A

    View Slide

  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

    View Slide

  93. Stack Depth Is 1 on Return

    View Slide

  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

    View Slide

  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

    View Slide

  96. And Others!

    View Slide

  97. Did we do a JIT?

    View Slide

  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

    View Slide

  99. How Do MJIT / YJIT Work?

    View Slide

  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;


    }

    View Slide

  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;


    }

    View Slide

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


    }

    View Slide

  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

    View Slide

  104. JIT Contract:




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

    View Slide

  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

    View Slide

  106. EC: Execution Context
    There is only one!
    EC
    def recursive(n)


    return if n == 0


    recursive(n - 1)


    end


    recursive(3)
    Sample Code

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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)

    View Slide

  111. 😈😈😈😈😈

    View Slide

  112. Fiddle::Pointer:


    Given an address, read memory

    View Slide

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

    View Slide

  114. Fiddle.dlwrap:


    Returns address of Ruby object

    View Slide

  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 `'


    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


    => #free=0x0000000000000000>


    >> ptr[16] = 'e'.bytes.first


    => 101


    >> str


    => "eello"
    Get the address
    Make a pointer
    Write some bytes
    Pro
    fi
    t

    View Slide

  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

    View Slide

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

    View Slide

  118. How do we know where?

    View Slide

  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


    }


    }


    }

    View Slide

  120. DWARF

    View Slide

  121. DWARF is just text

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  128. Pure Ruby JIT: 3 Parts

    View Slide

  129. RubyVM::InstructionSequence

    View Slide

  130. x86 Translation using Fisk

    View Slide

  131. OdinFlex / Fiddle

    View Slide

  132. TenderJIT!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  136. View Slide

  137. View Slide

  138. THANK YOU!!!!

    View Slide