This blog is part of our Ruby 2.6 series. Ruby 2.6.0 was released on Dec 25, 2018.

What is JIT?

JIT stands for Just-In-Time compiler. It converts repeatedly used code to bytecode which can be sent to processor directly hence saving time of compiling same piece of code again and again.

Ruby 2.6

MJIT is introduced in Ruby 2.6. It is most commonly known as MRI JIT or Method Based JIT.

It’s part of Ruby 3x3 project that was started by Matz. The name was to signify that Ruby 3.0 will be 3 times faster than Ruby 2.0 and it focused mainly on performance. Apart from performance it also aims for following things.

  1. Portability
  2. Stability
  3. Security

MJIT is still in development and hence MJIT is optional in Ruby 2.6. If you are running Ruby 2.6 then execute following commnad.

ruby --help

You will see following options.

--Jit-wait # Wait program execution until code compiles.
--jit-verbose=num # Level information MJIT compiler prints for Ruby program.
--jit-min-calls=num # Minimum count in loops for which MJIT should work.
--jit-max-cache
--jit-save-temps # Save compiled library to the file.

Vladimir Makarov proposed to improve performance by replacing VM instructions with RTL(Register Transfer Language) and introducing Method based JIT compiler.

Vladimir explained MJIT architecture in his RubyKaigi 2017 conference keynote.

Ruby’s compiler converts the code to YARV(Yet Another Ruby VM) instructions and then these instructions are run by the Ruby Virtual Machine. Code that is executed too often are converted to RTL instructions which runs faster.

Let’s take a look at how MJIT works.

# mjit.rb require 'benchmark' puts Benchmark.measure { def test_while start_time = Time.now i = 0 while i < 4 i += 1 end i puts Time.now - start_time end 4.times { test_while }
}

Let’s run this code with MJIT options and check what we got.

ruby --jit --jit-verbose=1 --jit-wait --disable-gems mjit.rb
Time taken is 4.0e-06
Time taken is 0.0
Time taken is 0.0
Time taken is 0.0 0.000082 0.000032 0.000114 ( 0.000105)
Successful MJIT finish

Nothing interesting right? And why is that? because we are iterating the loop for 4 times and default value for MJIT to work is 5. We can always decide after how many calls MJIT should work by providing --jit-min-calls=#number option.

Let’s tweak the program a bit so that MJIT gets some work to do.

require 'benchmark' puts Benchmark.measure { def test_while start_time = Time.now i = 0 while i < 4_00_00_000 i += 1 end puts "Time taken is #{Time.now - start_time}" end 10.times { test_while }
}

After running above code we can see some work done by MJIT.

Time taken is 0.457916
Time taken is 0.455921
Time taken is 0.454672
Time taken is 0.452823
JIT success (72.5ms): block (2 levels) in <main>@mjit.rb:15 -> /var/folders/v6/_6sh53vn5gl3lct18w533gr80000gn/T//_ruby_mjit_p66220u0.c
JIT success (140.9ms): test_while@mjit.rb:4 -> /var/folders/v6/_6sh53vn5gl3lct18w533gr80000gn/T//_ruby_mjit_p66220u1.c
JIT compaction (23.0ms): Compacted 2 methods -> /var/folders/v6/_6sh53vn5gl3lct18w533gr80000gn/T//_ruby_mjit_p66220u2.bundle
Time taken is 0.463703
Time taken is 0.102852
Time taken is 0.103335
Time taken is 0.103299
Time taken is 0.103252
Time taken is 0.103261 2.797843 0.005357 3.141944 ( 2.801391)
Successful MJIT finish

Here’s whats happening. Method ran 4 times and on 5th call it found that it is running same code again. So MJIT started a separate thread to convert the code into RTL instructions which created shared object library. Next threads took that shared code and executed directly. As we passed option --jit-verbose=1 we can see what MJIT did.

What we are seeing in output are followings.

  1. Time took to compile.
  2. What block of code is compiled by JIT.
  3. Location of compiled code.

We can open the file and see how MJIT converted piece of code to binary instructions but for that we need to pass another option which is --jit-save-temps and then just inspect those files.

After compiling code to RTL instructions, take look at the execution time. It dropped down to 0.10 ms from 0.46 ms. That’s a neat speed bump.

Here is comparation across some of the Ruby versions for some basic operations.

Ruby time comparison in different versions

Rails comparison on Ruby 2.5, Ruby 2.6 and Ruby 2.6 with JIT

Create a rails application with different Ruby versions and start a server. We can start rails server with JIT option as shown below.

RUBYOPT="--jit" bundle exec rails s

Now we can start testing performance on servers. What we found is that Ruby 2.6 is faster than Ruby 2.5, but enabling JIT in Ruby 2.6 is not adding more value to Rails application.

MJIT status and future directions

  • It’s in early development stage.
  • Doesn’t work on windows.
  • Needs more time to mature.
  • Needs more optimisations.
  • MJIT can use GCC or LLVM in the future C Compilers.

Further reading