A program is compiled at runtime using a different method from pre-execution compilation. This process is known as just-in-time compilation or dynamic translation.
In this post, we'll look at why JIT compilation can be a good choice for your Ruby on Rails app, before looking at some of the options available (YJIT, MJIT, and TenderJIT) and how to install them.
But first: how does JIT compilation work?
Just-in-time compilation is a method of running computer code that requires compilation while running a program.
This could entail translating the source code, but it's most frequently done by converting the bytecode to machine code, which is then run directly.
The code being executed is often continuously analyzed by a system using a JIT compiler. This identifies sections of code where the benefit of compilation or recompilation (in terms of speed) outweighs the cost.
JIT compilation combines some of the benefits (and shortcomings) of the two conventional methods for converting programs into machine code: interpretation and ahead-of-time compilation (AOT).
Roughly speaking, it combines the flexibility of interpretation with the speed of generated code, and the additional overhead of compiling and linking (not just interpreting).
JIT compilation is a type of dynamic compilation that enables adaptive optimization techniques, including dynamic recompilation and speed-ups tailored to certain microarchitectures. Due to a runtime system's ability to handle late-bound data types and impose security guarantees, dynamic programming languages like Ruby are particularly well-suited for interpretation and JIT compilation.
An optimizing compiler like GCC can more efficiently optimize instructions — a significant advantage of adopting a register-oriented architecture. Compilers operate on intermediate representation with register-based architecture.
Once your instructions reach an intermediate representation during compilation, GCC does additional passes to speed up the CPU's execution of your instructions.
Now let's explore the different JIT compilers available for Ruby — YJIT, MJIT, and TenderJIT — and how you can set them up.
Vladimir Makarov implemented MJIT, and it was the first compiler methodology implemented in Ruby based on the C language. It works with Ruby 2.6, uses YARV instructions, and compiles instructions often used in binary code.
For programs that are not input/output-bound, MJIT enhances performance.
YJIT is better than this original C-based compiler in terms of performance. Ruby 3's JIT is the quickest JIT that MRI has ever had, made possible by the excellent work of MJIT.
To use MJIT, you can enable the JIT in Ruby 2.6 and with the
ruby --jit app.rb
If you skip this part, MJIT will show an error.
clang: error: cannot specify -o when generating multiple output files MJIT warning: Making precompiled header failed on compilation. Stopping MJIT worker... MJIT warning: failed to remove "/var/folders/3d/fk_588wd4g12syc56pjqybjc0000gn/T//_ruby_mjit_hp25992u0.h.gch": No such file or directory Successful MJIT finish
A collection of JIT-specific settings included in Ruby 2.6 helps us understand how it functions. Run
ruby --help to view these options.
In short, MJIT executes in a different thread and is asynchronous. It will begin just-in-time compilation following the first five runs of a calculation.
A recent JIT compiler called YJIT was released with Ruby 3.1. It promises a lot of improvements and better performance. Still a work-in-progress project designed by Shopify with experimental results, it must be used with caution, especially on larger applications.
With that in mind, YJIT enhances the performance of Ruby on Rails applications. The majority of real-world software benefits from the fast warm-up and performance enhancements provided by the YJIT basic block versioning JIT compiler.
A JIT compiler will be gradually built into CRuby as part of the YJIT project, eventually replacing the interpreter for most of the code execution.
Official benchmarks — see 'YJIT: Building a New JIT Compiler for CRuby' — show that YJIT improved performance over the default CRuby interpreter by:
- 20% on railsbench
- 39% on liquid template rendering
- 37% on activerecord
Only about 79% of instructions in railsbench are executed by YJIT, and the rest run in the default interpreter.
This means that a lot still needs to be done to improve YJIT's current results.
Even so, YJIT performs at least as well as the interpreter on every benchmark, even on the hardest ones, and reaches near-peak performance after just one iteration of every benchmark.
Note: YJIT is currently limited to macOS and Linux on x86-64 platforms. Also, as mentioned, YJIT is not recommended for large applications (yet).
YJIT is disabled by default. If you want to enable it, first specify the
--yjit command-line option.
You need to check if it is installed, so run
ruby --enable-yjit -v. If
warning: unknown argument for --enable:yjit'` shows up, you have to install it.
irb and set
RUBY_YJIT_ENABLE=1. You can exit and now, you're ready to use YJIT. The command
ruby --enable-yjit -v must return something like
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [arm64-darwin21].
With a design largely based off YJIT, TenderJIT is an experimental JIT compiler for Ruby. What's different about TenderJIT is that it's written in pure Ruby.
This is a demo project and the aim is to ship it as a gem. In the meantime, you can experiment with it, but bear in mind it's still a work in progress. Ruby 3.0.2 or later is required for TenderJIT.
TenderJIT does not currently do method compilation automatically. To compile a method, you must manually configure TenderJIT.
Clone the repository and run the following commands:
$ bundle install
$ bundle exec rake test
You must set it manually on your code:
jit = TenderJIT.new
Each YARV instruction in the target method is read by TenderJIT, which then transforms it into machine code.
In this post, we've taken a quick look at three JIT compilers for Ruby — MJIT, YJIT, and TenderJIT — and how to set them up. Each of the options is experimental and comes with its own limitations.
However, YJIT is the most mature at the moment, and it has the biggest potential to grow and scale. It demonstrates better performance over the other Ruby JITs, was developed with Ruby 3.1.0, and is quickly becoming an important part of CRuby.
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!