The Ruby committers have again continued their annual holiday tradition of gifting us a new Ruby version: Ruby 2.6 was released today, including the long awaited Just-In-Time (JIT) compiler that the Ruby team has been working on for more than a year.
Just-In-Time compilation requires Ruby to spin up a compiler process on startup, and we're proud to say that this feature is supported today on Heroku thanks to the diligent efforts of our very own Richard Schneeman. We'd also like to thank fellow Herokai Nobuyoshi Nakada for his effort making sure the new JIT works well with all of the officially supported compilers: GCC, Clang and Microsoft Visual C++.
Using Ruby 2.6 on Heroku
You can start using the JIT with your Heroku applications today, just add Ruby 2.6 to your Gemfile:
source 'https://rubygems.org'
ruby '2.6.0'
Make sure you add the --jit
flag to your Procfile for any Ruby processes:
web: ruby --jit jit_test.rb
Alternatively you can set an environment variable for your application to use the JIT:
web: RUBYOPT=--jit rails server
Note: Using
heroku config
to specifyRUBYOPT
currently does not work, but will be supported soon
Please think carefully before running anything in production with the JIT enabled, and make sure you have a plan in place to measure the performance of your applications. This would be an excellent time to install New Relic if you haven't already.
MJIT, YARV and RTL
If you've been following along with the Ruby team's progress you know that Vladimir Makarov first proposed a method-based JIT (MJIT) for Ruby at RubyKaigi 2017. For some additional context around that proposal, and a deeper understanding of JIT compilers and why they're popular, you might enjoy this interview we conducted with Vlad directly after he left the stage in Hiroshima: MJIT: A Method-based Just-In-Time Compiler for Ruby.
Vlad's MJIT proposal also included a significant change to the way Ruby runs your code: replacing the existing intermediate representation (IR) known as YARV with another easier to optimize IR called RTL.
Alongside Vlad's work on MJIT another prolific Rubyist named Takashi Kokubun (Ruby committer, maintainer of ERB and HAML) began developing a more conservative JIT called YARV-MJIT. As you might deduce from the name, Kokubun's JIT implementation made use of the existing YARV instructions in Ruby, rather than replacing them with RTL as in Vlad's proposal.
I've just committed the initial JIT compiler for Ruby. It's not still so fast yet (especially it's performing badly with Rails for now), but we have much time to improve it until Ruby 2.6 (or 3.0) release. https://t.co/7mO5FZM80C
— k0kubun (@k0kubun) February 4, 2018
Given the level of risk involved with swapping out YARV for RTL, the Ruby team decided to move forward with Kokubun's approach, and today that work finally becomes available in Ruby 2.6 with the addition of a --jit
option.
What even is a JIT?
A JIT allows an interpreted language such as Ruby to optimize frequently run methods so they run faster for future calls. The implementation details differ between languages, but generally speaking the goal of a JIT is to skip some or all of the interpretation steps that would normally be required for these methods.
Why does Ruby want a JIT?
Several years ago Matz set a goal for the Ruby team to triple the speed of Ruby by the release of Ruby 3; he named this initiative Ruby 3x3.
There have been many performance improvements in Ruby since Matz set this ambitious goal, but we have plenty of work left to do. At this point in Ruby's development, the introduction of MJIT likely represents our best chance of making it to the finish line.
Last month at RubyConf Kokubun presented data that showed a 1.8x speed increase for Ruby 2.6 (as compared to Ruby 2.5) when using the new --jit
option with the popular optcarrot benchmark, a very impressive gain to be sure.
Unfortunately, many alternative benchmarks (e.g. Rails, Sidekiq) have seen decreased performance, presumably because they have a very large number of methods that are called frequently.
If you'd like to read more about Kokubun's benchmarking strategy check out his recent post: Ruby 2.6 JIT - Progress and Future.
Why is Rails slower?
Rails and similar projects with very large numbers of frequently called methods will experience slower performance using MJIT, because the process of optimizing an individual method is actually slower than interpreting that method directly. Ideally this slowdown is absorbed by the increased performance of future calls to the now compiled method, but for large numbers of methods this performance hit becomes significant.
Since all of this compilation happens when the methods are first called you'd think that over time Rails would eventually become faster, once your application “warms up”. However, because each of these methods initially consumes about 2MB of memory, the memory required to compile thousands of methods quickly approaches the bounds of most machines.
If you're curious about Rails performance specifically take a moment to read Noah Gibbs' article A Short Update: How Fast is Ruby 2.6.0rc1?
JIT Compaction
To help solve the many-method issue Kokubun introduced the concept of JIT compaction. As soon as the number of compiled methods approaches the default maximum cache size of 1000 methods, MJIT will combine those methods in memory to reduce their size. This change definitely helps the situation, but it's not enough just yet to make Rails more performant with MJIT.
Beyond the size of Rails there are issues with how Rails is actually implemented. The framework makes heavy use of wrapped core classes like HashWithIndifferentAccess
, and the present MJIT implementation is optimized to deal with the core objects themselves. The same is true of methods like blank?
that only exist in Rails; MJIT is prepared to optimize calls to the actual Ruby methods like empty?
, but Rails developers are much more likely to use blank?
for the added convenience, and this comes with additional overhead.
The future of the Ruby JIT
Kokubun anticipates that the --jit
option will eventually be removed and MJIT will be enabled by default. Given that the goal of implementing a JIT in the first place was to speed up Ruby for the most common use cases, it's not likely to happen before MJIT is able to at least match existing Rails performance. Despite those challenges, Kokubun anticipates that MJIT could become the default as soon as Ruby 2.7.
Congratulations to Kokubun, Vlad and everyone on the Ruby team for another successful release!
Want to Make a Contribution Yourself?
The best way to express your gratitude for Ruby is to make a contribution.
There are all sorts of ways to get started contributing to Ruby, if you're interested in contributing to Ruby itself check out the Ruby Core community page.
Another great way to contribute is by testing preview versions as they’re released, and reporting potential bugs on the Ruby issues tracker. Watch the Recent News page (RSS feed) to find out when new preview versions become available.
Thank you for reading and have a wonderful holiday!
<3 Jonan
Heroku Developer Advocate