In this post, you’ll learn about the different Ruby interpreters available.
Every popular programming language has multiple interpreters or compilers (in the case of compiled languages like C), but what’s an interpreter?
An interpreter is a program that reads your source code, converts it into a series of executable instructions & then runs them.
In other words: it’s like a compiler, but it runs your code directly, without producing an output file.
What production-ready interpreters do we have available in Ruby?
- MRI (The original implementation & what most people use)
- Rubinius
- JRuby
There are other interpreters, but most of them are experimental (like topaz) or not actively maintained (like IronRuby).
In the rest of this article I’m going to focus on explaining the main differences between MRI, Rubinius & JRuby so that you can be more informed about the different options available to you 🙂
Meet The Interpreters
Let’s start talking about MRI, the original and most popular interpreter.
MRI stands for “Matz’s Ruby Interpreter”, but some of the core developers prefer to call it “CRuby”. It was created (and is still maintained by) Yukihiro Matsumoto (Matz) in 1995 & it’s written entirely in C.
Then we have JRuby, which is written in Java & runs on the JVM (Java Virtual Machine). One thing you can do, that isn’t possible in any other Ruby interpreter, is to use Java libraries in your code.
And the last interpreter we are going to talk about is Rubinius. The main goal of Rubinius is to have a Ruby interpreter written in Ruby itself (but there are still some parts written in C++). I think it’s a great way to take a look at how some things work under the hood if you don’t want to deal with C or Java code.
Is Anything Missing?
What are the main differences in terms of features? Is there anything missing from JRuby or Rubinius that could prevent you from running them?
Well according to the Rubinius README, the main things missing are Refinements
& the TracePoint
module from the standard library. There are other things missing, but I think those two stand out the most.
How about JRuby?
JRuby 9.1 claims to be compatible with Ruby 2.3, but I can’t find more details about what level of compatibility we are talking about.
Comparing Performance
So what about performance? Is there a huge gap between the 3 main interpreters?
I ran some benchmarks for you using the latest versions of every interpreter, so you can see for yourself. The results are in iterations per second.
Code:
require 'benchmark/ips' Benchmark.ips do |x| x.time = 20 x.warmup = 3 x.report { (1..100).inject(:*) } end
MRI 2.3.1
36.776k i/s
JRuby 9.1.5 (OpenJDK 1.8)
83.200k i/s
Rubinius 3.6
65.062k i/s
This doesn’t mean that MRI is too slow for regular use or that JRuby is going to be the fastest on every situation so don’t choose your interpreter based on these results. The results change a lot depending on what code you are benchmarking (also one big goal for MRI 3.0 is to x3 performance!).
So if that is not a good way to choose, which interpreter should you be using? Well, most of the time you should be fine with MRI, but there are more differences worth exploring.
Error Output Differences
There are also some differences when it comes to error output & stack traces in particular.
Here is the same stack trace as it appears on each implementation.
def method_1 1.a end method_1
MRI
Rubinius
JRuby
The Rubinius backtrace looks the hardest to read to me (because of the extra noise), but the color helps a little 🙂
What do you think? Let me know in the comments!
What About The GIL?
Another important difference between MRI & other interpreters is that MRI has something called the GIL (Global Interpreter Lock).
The GIL is something used internally by MRI to simplify some multi-threading code, but this also has an impact on the code you write.
But before I expand on that, let me give you some required background on concurrency theory.
Threads can work in two ways: concurrency or parallelism.
Concurrency means that, while you can have multiple tasks active, only one can use the CPU. What happens is that the tasks take turns, similar to how process scheduling works. This is what you get with MRI, the job of the GIL is to only let one thread run at a time.
On the other hand you have parallelism, this is full-on multi-threading, where you can have multiple tasks running at the same time. This is the only way to take advantage of multi-core or multi-cpu systems.
So you may be asking, why is the GIL a thing? Well, concurrency is hard, there are many things that can go wrong (like dead locks & race conditions). So Matz decided many years ago (when multi-threading was not as prevalent) to include the GIL to avoid most of these issues.
In summary, what this means to you as a Ruby developer:
- You can still use Threads in MRI & they are still very effective for IO-heavy workloads.
- If you need true parallelism you may want to try Rubinius or JRuby as your main interpreter.
- A goal for Ruby 3.0 is to remove the GIL, so you may not need to switch interpreters anyway 🙂
Conclusion
In this post you have learned about the different Ruby interpreters available (MRI, Rubinius & JRuby) and how they differ from each other.
If you found this useful don’t forget to click on those share buttons 🙂
I did not get the same results as you for the simple benchmark. I ran them myself with latest versions of all three impls, because I was confused why JRuby performance was not higher.
Here’s MRI 2.3.1, Rubinius 3.60 (not 3.6 as in your post), and JRuby 9.1.5.0:
My numbers put JRuby at nearly 5x faster than MRI. What platform are you on?
I ran the benchmarks on Manjaro Linux (kernel version 4.4.19) under a virtual machine (VirtualBox 5.x), so that probably has something to do with the results being different 🙂
Let me know if you need more details.
Ahhh that could explain it. Depending on how the virtualization is done, the JVM can have degraded performance. Thanks!
I just noticed your numbers for Rubinius 3.60 are way off too. Why is that? MRI was almost more than 1.5x faster for me.
Not sure what’s going on there. Is there anything I can do to find out?
Here is some additional info in case it helps:
llvm-config –version: 3.5.2
clang version: 3.8.1
Maybe you can just confirm those numbers are actually 3.60. I thought I saw “3.6” in the post before.
The numbers on the post are for “3.6” (I did
ruby-install rbx 3.6
). Sorry, I thought 3.60 & 3.6 were the same thing 🙂