RubyGuides
Share this post!

What Is MJIT in Ruby 2.6 & How Does It Work?

Ruby’s performance has been improving a lot, version after version… and the Ruby development team is making every effort to make Ruby even faster!

One of these efforts is the 3×3 project.

The goal?

Ruby 3.0 will be 3 times faster than Ruby 2.0.

Part of this project is the new MJIT compiler, which is the topic of this article.

MJIT Explained

MJIT stands for “Method Based Just-in-Time Compiler”.

What does that mean?

Ruby compiles your code into YARV instructions, these instructions are run by the Ruby Virtual Machine.

The JIT adds another layer to this.

It will compile instructions that are used often into binary code.

The result is an optimized binary which runs your code faster.

How It Works

Let’s explore how MJIT works to understand it better.

You can enable the JIT with Ruby 2.6 & the --jit option.

Like this:

ruby --jit app.rb

Ruby 2.6 comes with a set of JIT-specific options that will help us discover exactly how it works. You can see these options by running ruby --help.

Here’s a list of the options

  • –jit-wait
  • –jit-verbose
  • –jit-save-temps
  • –jit-max-cache
  • –jit-min-calls

This verbose option looks like a good starting point!

We are also going to be using --jit-wait, this makes Ruby wait until the compilation of JIT code is done before running it.

During normal operation the JIT compiles code in a worker thread & it doesn’t wait for it to finish.

Here’s the command you can run to test this:

ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "4.times { 123 }"

This prints:

Successful MJIT finish

Well, that’s not very interesting, is it?

The JIT is not doing anything.

Why?

Because by default, the JIT only comes into action when a method is called 5 times (jit-min-calls) or more.

If we run this:

ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "5.times { 123 }"

Now we get something interesting:

JIT success (32.1ms): block in <main>@-e:1 -> /tmp/_ruby_mjit_p13921u0.c

What does this say?

The JIT compiled a block because we called it 5 times, this tells you:

  • How long it took to compile (32.1ms),
  • Exactly what was compiled (block in <main>)
  • The file that was generated (/tmp/_ruby_mjit_p13921u0.c) as the source for this compilation

This file is C source code which is compiled into an object file (.o) & then into a shared library file (.so).

You can get access to these files if you add the --jit-save-temps option.

Here’s an example:

MJIT Code

This is my current understanding of how the JIT works:

  1. Count method calls
  2. When one method is called 5 times (default for jit-min-calls) trigger JIT
  3. A C file that contains the instructions for this method is created (these are YARV instructions, but inlined)
  4. Compilation happens in the background (unless --jit-wait) using a regular C compiler like GCC
  5. When the compilation is done the resulting shared library file is used when this method is called

Let’s see how effective this is.

Testing MJIT: Is It Really Faster?

The goal of MJIT is to make Ruby faster.

How good is it doing that right now?

Let’s find out!

First, microbenchmarks:

Benchmark Results (Compared to Ruby 2.6 without JIT)
while 8x faster
while with string append 10% faster
while with multiplication (Integer) 4x faster
while with multiplication (Bignum) 20% slower
string upcase 10% faster
string match 2% slower
string match? 10% faster
array with 10k random numbers 20% faster

It seems like performance is all over the place, but there is something we can deduce from this…

MJIT really likes loops!

But how does it fare with a more complex application?

Let’s try with a simple Sinatra app:

require 'sinatra'

get '/' do
  "apples, oranges & bananas"
end

It may not look like much, but this little bit of code runs over 500 different methods. Enough to give the JIT some work to do!

To be specific, this is Sinatra 2.0.4 with Thin 1.7.2.

You can run the benchmark with this command (apache bench):

ab -c 20 -t 10 http://localhost:4567/

These are the results:

MJIT Benchmark

You can tell from these that Ruby 2.6 is faster than 2.5, but enabling the JIT makes Sinatra 11% slower!

Why?

I don’t know, it may be because overhead introduced by JIT, or because the code is not well optimized.

My testing with a C profiler (callgrind) reveals that the use of JIT optimized code (the compiled C files that we discovered earlier) is very low for Sinatra (less than 2%), but it’s very high (24.22%) for the while statement that gets a 8x speed boost.

Results for the while benchmark with JIT:

MJIT Callgrind Results

Results for Sinatra benchmark with JIT:

MJIT Sinatra Callgrind Results

This may be part of the reason, I’m not a compiler expert so I can’t make any conclusions from this.

Summary

MJIT is a “Just-in-Time Compiler” available in Ruby 2.6, it can be enabled with the --jit flag. MJIT is promising & can speed up some small programs, but there is still a lot of work to do!

If you liked this article don’t forget to share it with your Ruby friends 🙂

Thanks for reading.

Leave a Comment:

2 comments
Tim says 3 weeks ago

For the last few minor releases I’ve been compiling ruby with jemalloc for improved memory allocation. Would this interfere with jit in ruby 2.6?

Reply
    Jesus Castello says 3 weeks ago

    That’s an interesting question Tim.

    As far as I understand I don’t think it will interfere because the JIT doesn’t change how memory allocation works. The best way to find out is to download a preview version of Ruby 2.6 & try it out 🙂

    Reply
Add Your Reply