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

Method JIT Compiler for MRI

Method JIT Compiler for MRI

RubyElixirConf 2018
https://2018.rubyconf.tw/

Takashi Kokubun

April 27, 2018
Tweet

More Decks by Takashi Kokubun

Other Decks in Programming

Transcript

  1. Method JIT Compiler for MRI RubyElixirConf Taiwan 2018 ~ Optimizations

    in Ruby 2.6.0 preview1, 2 ~ @k0kubun / Treasure Data Inc.
  2. October 2017: YARV-MJIT 0QUDBSSPUXJUI[email protected] GQT     

    Ruby 2.0 Ruby 2.5 YARV-MJIT RTL MJIT     https://github.com/k0kubun/yarv-mjit/tree/master-171211#optcarrot-benchmark
  3. Micro benchmark: while 5.7x faster 2.6.0 Preview1 2.6.0 Preview2 ?

    https://benchmark-driver.github.io/benchmarks/mjit/commits.html
  4. 0QUDBSSPUXJUI[email protected] GQT      Ruby 2.0 trunk

    trunk+JIT RTL+JIT Ruby 3x3      But… we’re still far from Ruby 3x3 https://gist.github.com/k0kubun/7074ad434d0affd1bd98edaaa011ac1d 39fps to go
  5. How to get there? Just inlining method doesn’t help if

    code is too complex We need more effort to exploit C compiler optimizations Let’s see what we’ve done so far
  6. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code ISeq Compile putself send :bar, cache: nil leave
  7. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code putself send :bar, cache: nil leave ISeq Ruby VM Program Counter Interpret
  8. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code putself send :bar, cache: nil leave ISeq Ruby VM Program Counter Call putself() { val = GET_SELF(); } C code for instruction
  9. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code putself send :bar, cache: nil leave ISeq Ruby VM Program Counter Interpret
  10. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code putself send :bar, cache: nil leave ISeq Ruby VM Program Counter Call send(cache) { search_method(cache); CALL_METHOD(cache); } C code for instruction
  11. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code putself send :bar, cache: nil leave ISeq Ruby VM Program Counter Call send(cache) { search_method(cache); CALL_METHOD(cache); } C code for instruction Ruby method push C method call attr_reader attr_writer . . . Which type will be called?
  12. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code putself send :bar, cache: Ruby leave ISeq Ruby VM Program Counter Call send(cache) { search_method(cache); CALL_METHOD(cache); } C code for instruction Store C function pointer w/ class timestamp Ruby method push C method call attr_reader attr_writer . . .
  13. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code putself send :bar, cache: Ruby leave Ruby VM Program Counter Call send(cache) { search_method(cache); CALL_METHOD(cache); } C code for instruction Ruby method push Dispatch it by calling function pointer (compiler can't optimize)
  14. 1. Basic inlining of Ruby method (r62197) def foo bar

    end Ruby code putself send :bar, cache: Ruby leave Ruby VM Program Counter Call send(cache) { search_method(cache); CALL_METHOD(cache); } C code for instruction Ruby method push Ruby method push In JIT, we can inline this operation by checking cache in ISeq ISeq
  15. 1. Basic inlining of Ruby method (r62197) Using “method cache”,

    we can bypass method dispatch and inline the C function to push Ruby method frame If it's inlined, C compiler can apply various optimizations to Ruby method call, which is known as slow Optcarrot: 53.84fps -> 57.52fps
  16. 2. Bypass Array/Hash check for #[] (r62398) optimized_#[](recv, key) {

    if recv.is_a?(Array) { fast_Array#[](recv, key); } else if recv.is_a?(Hash) { fast_Hash#[](recv, key); } else { dispatch(recv, #[], key); } }
  17. 2. Bypass Array/Hash check for #[] (r62398) optimized_#[](recv, key) {

    if recv.is_a?(Array) { fast_Array#[](recv, key); } else if recv.is_a?(Hash) { fast_Hash#[](recv, key); } else { dispatch(recv, #[], key); } } array = [1,2,3] array[1]
  18. 2. Bypass Array/Hash check for #[] (r62398) optimized_#[](recv, key) {

    if recv.is_a?(Array) { fast_Array#[](recv, key); } else if recv.is_a?(Hash) { fast_Hash#[](recv, key); } else { dispatch(recv, #[], key); } } hash = { foo: 1} hash[:foo]
  19. 2. Bypass Array/Hash check for #[] (r62398) optimized_#[](recv, key) {

    if recv.is_a?(Array) { fast_Array#[](recv, key); } else if recv.is_a?(Hash) { fast_Hash#[](recv, key); } else { dispatch(recv, #[], key); } } def show params[:id] end ActionController::Parameters#[]
  20. 2. Bypass Array/Hash check for #[] (r62398) optimized_#[](recv, key) {

    if recv.is_a?(Array) { fast_Array#[](recv, key); } else if recv.is_a?(Hash) { fast_Hash#[](recv, key); } else { dispatch(recv, #[], key); } } def show params[:id] end ActionController::Parameters#[] These checks are NOT needed for classes other than Array, Hash
  21. 2. Bypass Array/Hash check for #[] (r62398) jit_#[](recv, key) {

    dispatch(recv, #[], key); } def show params[:id] end ActionController::Parameters#[]
  22. 2. Bypass Array/Hash check for #[] (r62398) Ruby always optimizes

    #[] for Array/Hash, but it’s suboptimal for other classes JIT removes the guard for Array/Hash by seeing call cache, and also inlines pushing a method frame The same optimization can be applied to other methods later
  23. 3. Inline Array#[] with Integer (r62388) optimized_#[](recv, key) { if

    recv.is_a?(Array) { fast_Array#[](recv, key); // extern } else if recv.is_a?(Hash) { fast_Hash#[](recv, key); } else { dispatch(recv, #[], key); } } It's not inlined and optimized well by compiler
  24. 3. Inline Array#[] with Integer (r62388) optimized_#[](recv, key) { if

    recv.is_a?(Array) { if key.is_a?(Integer) { Array#[Integer](recv, key); // inline } else { fast_Array#[](recv, key); // extern } } else if recv.is_a?(Hash) { fast_Hash#[](recv, key); } else { dispatch(recv, #[], key); } } This special path is inlined and optimized well on JIT
  25. 3. Inline Array#[] with Integer (r62388) Currently “JIT header“ has

    limited definitions of C functions in Ruby core I inlined a part of Array#[] definition, and then C compiler could optimize the code Optcarrot: 54.93fps -> 58.41fps
  26. 2.6.0-Preview1 wrap up I mainly worked for portability, stability, maintainability

    Fix SEGV and deadlock, remove broken optimizations… Notable optimizations were only 3, so it wasn't fast yet
  27. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave
  28. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave Ruby VM Program Counter Stack Pointer VM stack empty
  29. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave Ruby VM Program Counter Stack Pointer VM stack 1
  30. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave Ruby VM Program Counter Stack Pointer VM stack 1
  31. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave Ruby VM Program Counter Stack Pointer VM stack 1 2
  32. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave Ruby VM Program Counter Stack Pointer VM stack 1 2
  33. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave Ruby VM Program Counter Stack Pointer VM stack 3
  34. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave Ruby VM Program Counter Stack Pointer VM stack 3
  35. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave Ruby VM Program Counter Stack Pointer VM stack 3 How to skip the stack pointer motion in JIT?
  36. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave jit_three() { } JIT-ed code: before
  37. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave jit_three() { *sp = 1; sp++; } JIT-ed code: before
  38. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave jit_three() { *sp = 1; sp++; *sp = 2; sp++; } JIT-ed code: before
  39. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave jit_three() { *sp = 1; sp++; *sp = 2; sp++; *(sp-2) = opt_plus( *(sp-2),*(sp-1)); sp--; } JIT-ed code: before
  40. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave jit_three() { *sp = 1; sp++; *sp = 2; sp++; *(sp-2) = opt_plus( *(sp-2),*(sp-1)); sp--; return *(sp-1); } JIT-ed code: before
  41. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave jit_three() { *sp = 1; sp++; *sp = 2; sp++; *(sp-2) = opt_plus( *(sp-2),*(sp-1)); sp--; return *(sp-1); } JIT-ed code: before jit_three() { VALUE stack[2]; stack[0] = 1; stack[1] = 2; stack[0] = opt_plus( stack[0], stack[1]); return stack[0]; } JIT-ed code: after
  42. 1. Use C local variable for VM stack (r62655) def

    three 1 + 2 end Ruby code ISeq putobject 1 putobject 2 opt_plus leave jit_three() { *sp = 1; sp++; *sp = 2; sp++; *(sp-2) = opt_plus( *(sp-2),*(sp-1)); sp--; return *(sp-1); } JIT-ed code: before jit_three() { VALUE stack[2]; stack[0] = 1; stack[1] = 2; stack[0] = opt_plus( stack[0], stack[1]); return stack[0]; } JIT-ed code: after Array local variable This seems okay for just "1 + 2", but...
  43. 1. Use C local variable for VM stack (r62655) def

    err raise 'error' end def three 1 + (err rescue 2) end Ruby code
  44. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C VM stack empty
  45. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C ruby_vm() (setjmp called) VM stack empty
  46. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C ruby_vm() (setjmp called) jit_three() stack[nil, nil] in jit_three() VM stack empty
  47. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C ruby_vm() (setjmp called) jit_three() stack[1, nil] in jit_three() Push 1 to array local variable VM stack empty
  48. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C ruby_vm() (setjmp called) jit_three() stack[1, nil] in jit_three() jit_err() VM stack empty
  49. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C ruby_vm() (setjmp called) jit_three() stack[1, nil] in jit_three() jit_err() rb_raise() (call longjmp) VM stack empty
  50. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C ruby_vm() (setjmp called) jit_three() stack[1, nil] in jit_three() jit_err() rb_raise() (call longjmp) longjmp purges JIT-ed frames VM stack empty
  51. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C ruby_vm() (setjmp called) jit_three() stack[1, nil] in jit_three() jit_err() rb_raise() (call longjmp) VM stack empty 2
  52. 1. Use C local variable for VM stack (r62655) def

    err # JIT-ed raise 'error' end def three # JIT-ed 1 + (err rescue 2) end Ruby code main() Call stack in C ruby_vm() (setjmp called) jit_three() stack[1, nil] in jit_three() jit_err() rb_raise() (call longjmp) VM stack empty 2 VM Stack doesn't have 2 values => SEGV 1 is expired
  53. 1. Use C local variable for VM stack (r62655) When

    "catch table" (rescue, ensure, etc.) does not exist, we don't need to resurrect stack values on exception So we can use just C local variables to reproduce the stack of Ruby VM only when catch table does not exist Stack pointer is not moved and compiler can inline values Optcarrot: 57.13fps -> 62.14fps
  54. 2. Bypass setjmp for yield (r62643) setjmp is slow If

    JIT-ed code is directly called from VM (no C function frames are created yet), we don’t need to call setjmp again Now yield is 1.3x faster than a non-JIT-ed case
  55. 3. Skip moving program counter (r62678) def err raise 'error'

    end def three 1 + (err rescue 2) end Ruby code
  56. 3. Skip moving program counter (r62678) def err raise 'error'

    end def three 1 + (err rescue 2) end Ruby code Ruby call stack #three Program Counter
  57. 3. Skip moving program counter (r62678) def err raise 'error'

    end def three 1 + (err rescue 2) end Ruby code Ruby call stack #three Program Counter
  58. 3. Skip moving program counter (r62678) def err raise 'error'

    end def three 1 + (err rescue 2) end Ruby code Ruby call stack #three Program Counter
  59. 3. Skip moving program counter (r62678) def err raise 'error'

    end def three 1 + (err rescue 2) end Ruby code Ruby call stack #three #err Program Counter Program Counter
  60. 3. Skip moving program counter (r62678) def err raise 'error'

    end def three 1 + (err rescue 2) end Ruby code Ruby call stack #three #err Program Counter Program Counter
  61. 3. Skip moving program counter (r62678) def err raise 'error'

    end def three 1 + (err rescue 2) end Ruby code Ruby call stack #three #err Program Counter Program Counter #raise Program Counter longjmp
  62. 3. Skip moving program counter (r62678) def err raise 'error'

    end def three 1 + (err rescue 2) end Ruby code Ruby call stack #three #err Program Counter Program Counter #raise Program Counter Program counter is used to resurrect the position after longjmp
  63. 3. Skip moving program counter (r62678) Same as the stack

    value's situation, we don't move the program counter only when catch table does not exist (rescue, ensure, etc.) Optcarrot: 64.92fps -> 68.08fps
  64. 4. Force inlining arithmetic instructions (r62677) C compiler has a

    threshold of function size to be inlined Some Ruby's instructions (+, -, *, /, ...) are too large to be inlined by default, so I applied an "always inline" attribute In the future, we should reduce the size of code instead Optcarrot: 60.19fps -> 64.92fps
  65. 5. Force inlining ivar instructions (r62693) Not only arithmetic instructions,

    but also instructions for instance variable are large too, so I force-inlined it Optcarrot: 67.04fps -> 68.20fps
  66. 6. Disable stack consistency check (r63092) Ruby VM is always

    asserting the size of stack when returning from a method, and it's slow We can skip it on JIT because it's already checked by VM Optcarrot: 67.43fps -> 69.92fps
  67. 7. Inline attr_reader method call (r63212) . def foo bar

    end Ruby code putself send :bar, cache: nil leave ISeq Ruby VM Program Counter Call send(cache) { search_method(cache); CALL_METHOD(cache); } C code for instruction Ruby method push C method call attr_reader attr_writer . .
  68. 7. Inline attr_reader method call (r63212) def foo bar end

    Ruby code putself send :bar, cache: attr leave ISeq Ruby VM Program Counter Call send(cache) { search_method(cache); CALL_METHOD(cache); } C code for instruction Ruby method push C method call attr_reader attr_writer . . .
  69. 7. Inline attr_reader method call (r63212) def foo bar end

    Ruby code putself send :bar, cache: attr leave ISeq Ruby VM Program Counter Call send(cache) { search_method(cache); CALL_METHOD(cache); } C code for instruction get_istance_variable() attr_reader
  70. 7. Inline attr_reader method call (r63212) Using call cache in

    the same way as Ruby method, we can fully inline attr_reader without large compilation time The cost becomes the same as reference to normal instance variables Calling attr_reader is made 4x faster
  71. 2.6.0-Preview2 (trunk) wrap up I've mainly worked on performance because

    it's useless if it's slow Generated code is much simplified and made fast by removing program counter and stack pointer motions But it still has some complexity and it blocks significant performance improvement by Ruby method inlining
  72. 1. Deoptimization by longjmp We can generate aggressive code and

    cancel all JIT-ed calls by longjmp when something unexpected happens I’m going to remove guard for TracePoint and cancel it later It should also be used when all method caches are purged
  73. 2. Instruction specialization for types Currently the same code is

    generated for both Hash#[] and Array#[] We need some instrumentation to detect the type which is passed to an optimized instruction Vladimir's RTL instruction achieves this by dynamic modification of instruction
  74. 3. Multi-tier JIT Some other languages have multiple stages for

    JIT Depending on how frequently it's called, it may be better to balance compilation time and optimization level Vladimir is working on light JIT compilation Sometimes people deploy an application every 10 minutes
  75. 4. Profile-guided JIT C compiler has a feature to profile

    compiled code and generate faster code using the profiling result Using the multi-tier JIT, we may be able to profile code in the first tier and generate faster code in the second tier
  76. 5. Better JIT scheduler for Rails In Rails, an application

    becomes slower only during JIT compilation happens The possible cause might be the number of methods to be JIT-ed, compared to some other benchmarks Possibly we should reduce the number of methods to be JIT-ed or reduce frequency of JIT compilation
  77. 6. Ruby / C method inlining I already succeeded to

    implement Ruby method inlining, but it increases compilation time I have ideas to implement C method inlining, but which method to be inlined should be solved first
  78. 7. Exploit more C compiler optimizations Loop invariant motion Folding

    Ruby's constant Type check removal by type inference Reduce unnecessary memory accesses to VM registers
  79. Conclusion 2.6.0-preview2 will be much faster than 2.6.0-preview1 (Still not

    ready for Rails) We still have so many things to be done for Ruby 3x3