Project

General

Profile

How to use Ruby's JIT compiler

Project status

Experimental.

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

Basic usage

--jit

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

$RUBYOPT

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

Debugging

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 */

#undef OPT_CHECKED_RUN
#define OPT_CHECKED_RUN 0

VALUE
_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

Advantages

  • 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

Disadvantages

  • 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: https://github.com/vnmakarov/ruby/tree/rtl_mjit_branch#mjit-organization

  _______     _________________
 |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
    interpretation
    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