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
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
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
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?
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 . . .
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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?
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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