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

Why Ruby's JIT was slow / RubyKaigi Takeout 2021

Takashi Kokubun
September 08, 2021

Why Ruby's JIT was slow / RubyKaigi Takeout 2021

RubyKaigi Takeout 2021

Takashi Kokubun

September 08, 2021
Tweet

More Decks by Takashi Kokubun

Other Decks in Programming

Transcript

  1. Self introduction • GitHub, Twitter: @k0kubun • Ruby committer ◦

    JIT ◦ IRB: Color, ls, show_source ◦ Struct keyword_init • Treasure Data
  2. Why was Ruby's JIT slow? • The "MJIT" architecture was

    making Rails slow ◦ MJIT: C compiler + dlopen ◦ A lot of duplications in generated codes ▪ Ruby 3.0 fixed it ▪ Ruby 3.1 will have a better default config for it
  3. Other drawbacks of MJIT • Too slow compilation ◦ 5

    min to fully warm up 1,000 methods on Railsbench • Too large compilation overhead • PIC is slower by several cycles
  4. JIT's architecture matters! • It took 3 years to fix

    MJIT's bottleneck ◦ and still other drawbacks remain • It impacts your daily Ruby usage ◦ MJIT will make your Rails app slower during long warm-up ◦ MJIT requires a C compiler on runtime
  5. The goal of this talk • Discuss the JIT architecture

    of Ruby ◦ It will impact your future use of Ruby ◦ JIT authors could reduce development effort
  6. Layers of concerns VM JIT compiler Codegen RTL YARV RTL-MJIT

    MJIT (C compiler) YARV-MJIT (mjit_compile.c) MIR YJIT yjit_codegen.c MIR-based JIT Ruby 2.6~3.0 Feature #12589 MIR YJIT
  7. Discussion points • Maintainability • Internal Representation • How to

    compile and generate code • What optimization is feasible
  8. Why did we choose MJIT? • One reason: maintainability ◦

    You can use gdb and see C code while debugging JIT-ed methods
  9. Automatic support of new instructions • Support new instructions automatically

    ◦ Koichi's idea ◦ This may be helpful for any JIT • People think MJIT pastes C code and lets GCC do everything, but it’s wrong ◦ GCC alone can’t perform most of Ruby-specific optimizations
  10. Language to implement JIT: C vs Ruby • Use Ruby

    to write the JIT compiler? ◦ Ractor: always multi-ractor or create and stop every time? ◦ Inter-process communication with a JIT process?
  11. YARV vs RTL • YARV-MJIT is no longer slower than

    RTL-MJIT ◦ We didn’t need to rewrite the VM for JIT’s performance.
  12. Speculative instructions • These work like a profiler of runtime

    information • Alternatively, we could let JIT generate code for profiling ◦ The current MJIT generates the most speculative code first, and then recompile code with some optimizations disabled when cancelled ◦ YJIT's basic block versioning also profiles type information, etc.
  13. Compilation: Sync vs Async • MJIT: Concurrently JIT-compile methods in

    an MJIT worker thread • YJIT: Ruby threads JIT-compile methods during execution
  14. Compilation speed • MJIT: 50~200ms for a single compile, and

    minutes for compaction • YJIT, MIR: < 1ms
  15. Code generation • C compiler ◦ Slow startup • LLVM

    ◦ Binary size, build complexity • MIR • YJIT's assembler
  16. JIT code dispatch • call vs jmp (direct threading) ◦

    jmp is hard for MJIT ◦ But fortunately call seems faster, even in YJIT
  17. Deoptimization • On-stack replacement ◦ mprotect + SEGV handler ◦

    Code patching • It’s hard for MJIT to manipulate low-level information
  18. Method frame skip • We already have one since Ruby

    2.7 ◦ We also supported frame skip of more methods in Ruby 3.0 • Next: Lazy method frame push ◦ This is probably feasible in MJIT as well
  19. Conclusion • JIT's architecture may impact: ◦ When you can

    use it ◦ Warmup speed ◦ Performance of VM and JIT ◦ Build and runtime dependencies