How to use Ruby's JIT compiler

Project status


We fixed most of known bugs and portability issues on Windows.
While we achieved 1.7x performance improvement in CPU-intensive benchmark,
this still may NOT improve performance for real-world workloads like web applications.

Basic usage


This feature is disabled by default since it's still experimental. When you execute ruby command, you need to pass --jit flag.

$ ruby --jit script.rb


When you're not executing ruby command to run your application, you can pass any JIT-related flag on $RUBYOPT.

$ RUBYOPT="--jit" bin/rails s


If you want to see/debug what's happening behind it, use "--jit-verbose=1" or "--jit-verbose=2".
If you use "--jit-save-temps", you can check temporary files which are deleted during one execution.

$ ruby --disable-gems --jit-verbose=1 --jit-save-temps -e "def a; nil; end; a;a;a;a;a; RubyVM::MJIT.pause"
JIT success (19.3ms): a@-e:1 -> /tmp/_ruby_mjit_p8160u0.c
Successful MJIT finish

$ head -n11 /tmp/_ruby_mjit_p8160u0.c
#include "/tmp/_ruby_mjit_hp8160u0.h"
/* a@-e:1 */


_mjit0(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp)
    VALUE stack[1];
    static const VALUE *const original_body_iseq = (VALUE *)0x55d1ababf530;

Note that a method is queued to be JIT-ed when it's called 5 times.
RubyVM::MJIT.pause is a method to wait for JIT compilations and then shutdown a thread to run C compilers for JIT (call RubyVM::MJIT.pause to restart the thread).

See "ruby --help" to know other options.

Performance characteristics


  • Overhead of Virtual Machine interpretation is always eliminated by JIT's very nature
  • Basic instructions (+, -, *, /, [], ...) on core classes (Integer, String, Array, Hash, ...) are optimized more
  • Ruby method call is optimized using method cache information, especially when JIT-ed method is called by JIT-ed method
  • Instance variable access is optimized using inline cache information


  • Overhead of loading JIT-ed code is always added.
    • This overhead could be bigger than performance gain by JIT, especially on small methods.
    • The overhead is increased especially when there are many methods to be JIT-ed.
  • C compiler processes can steal CPU / memory resources while JIT compilation is still happening.
  • Calling non-JIT-ed Ruby method from JIT-ed method has overhead to invoke new Virtual Machine.

MJIT organization

The current architecture of Ruby's JIT is called "MJIT", which is proposed by Vladimir Makarov [Feature #12589].

Then it's merged as a more conservative form in [Feature #14235].
So it's slightly different from the original design in:

  _______     _________________
 |header |-->| minimized header|
 |_______|   |_________________|
               |                         MRI building
               |                         MRI execution
 |             |     |
 |          ___V__   |  CC      ____________________
 |         |      |----------->| precompiled header |
 |         |      |  |         |____________________|
 |         |      |  |              |
 |         | MJIT |  |              |
 |         |      |  |              |
 |         |      |  |          ____V___  CC  __________
 |         |______|----------->| C code |--->| .so file |
 |                   |         |________|    |__________|
 |                   |                              |
 |                   |                              |
 | MRI machine code  |<-----------------------------
 |___________________|             loading

  • MJIT is a method JIT (one more reason for the name)

  • An important organization goal is to minimize the JIT compilation time

  • To simplify JIT implementation the environment (C code header needed
    to C code generated by MJIT) is just vm.c file

  • A special Ruby script minimize the environment

    • Removing about 90% declarations
  • MJIT has a thread to do parallel compilation

    • It prepares a precompiled code of the minimized header
      • It starts at the MRI execution start
    • It generates PIC object files of ISEQs
      • They start when the precompiled header is ready
      • They take ISEQs from a priority queue unless it is empty.
      • They translate ISEQs into C-code using the precompiled header, call CC and load PIC code when it is ready
  • MJIT put ISEQ in the queue when ISEQ is called

  • MJIT can reorder ISEQs in the queue if some ISEQ has been called many
    times and its compilation did not start yet or we need the ISEQ code
    for AOT

  • MRI reuses the machine code if it already exists for ISEQ

  • All files are stored in /tmp. On modern Linux /tmp is a file
    system in memory

  • The machine code execution can stop and switch to the ISEQ
    if some condition is not satisfied as the machine
    code can be speculative or some exception raises

  • When TracePoint is enabled, all currently executed JIT functions are
    canceled and the corresponding ISEQs continue their execution in the interpreter mode

  • MJIT options can be given on the command line or by environment
    variable RUBYOPT

Related resources