full diff1: 55367b3...22d944c

Description

This PR replaces the current implementation of MJIT with a new JIT called “RJIT” 2.

  • RJIT uses a pure-Ruby assembler to generate native code
    • MJIT requires a C compiler at runtime. YJIT requires a Rust compiler at build time. RJIT doesn’t require them.
    • This means that RJIT’s warmup could be slower than YJIT, but it’s still much faster than MJIT’s.
  • The code generated by RJIT looks very similar to YJIT
    • In fact, many methods are direct translations of the Rust code into Ruby.
    • This allows us to simplify the Ruby VM by removing MJIT-specific implementations.
    • We could do some early experiments for YJIT in RJIT too if we want.

See the ticket for motivation and further details: [Feature #19420]

Benchmark

I benchmarked the interpreter, YJIT, RJIT, and MJIT with yjit-bench.

Headline

RJIT’s performance is still nowhere near YJIT’s, but notably RJIT outperforms MJIT in all headline benchmarks, which are considered the most real-world workloads. RJIT gives a small speedup on railsbench even with yjit-bench’s short warmup.

output_543

Other

Sometimes MJIT is still better than RJIT. However, RJIT outperforms both YJIT and MJIT on Optcarrot, which was the benchmark used for the Ruby 3×3 milestone.

output_542

Micro

30k_ifelse and 30k_methods are the things that YJIT is very good at, but RJIT outperforms YJIT on them. This seems to be because YJIT chose to interleave inline code and outlined code for Code GC and arm64’s performance whereas RJIT doesn’t do that. This is a good reminder of the code layout’s impact.

output_544

Footnotes

  1. I merged this branch in multiple batches because pushing hundreds of commits at once pressures our notification system a bit. However, an auto-format commit interrupted the operation, so I needed to resolve the conflict and this PR has only the diff after that commit.

  2. This PR doesn’t rename the interface and internal names from MJIT to RJIT yet, but a separate PR will do that soon.

Read More