Real Parallel Threading with JRuby and Rubinius

So MRI has a GIL. The GIL prevents real parallel threading. What about JRuby and Rubinius?

JRuby and Rubinius don’t have a GIL. This means they do allow for real parallel execution of Ruby code.

Proof

Before I get into any more technical explanations, I want to prove what I’m saying!

The following benchmark calculates 1 million prime numbers 10 times. I chose this particular example because it executes a lot of Ruby code, and because it’s easy to spread the work across multiple threads.

It uses Ruby’s Prime library to generate the primes. The first benchmark block does it ten times in one thread; the second block does it two times in each of five threads.

require 'benchmark'
require 'prime'

primes = 1_000_000
iterations = 10

num_threads = 5
iterations_per_thread = iterations / num_threads

# warmup (initialize the underlying singleton)
Prime.each(primes) { }

Benchmark.bm(15) do |x|
  x.report('single-threaded') do
    iterations.times do
      Prime.each(primes) { }
    end
  end

  x.report('multi-threaded') do
    num_threads.times.map do
      Thread.new do
        iterations_per_thread.times do
          Prime.each(primes) { }
        end
      end
    end.each(&:join)
  end
end

Here are the results across MRI, JRuby, and Rubinius:

The various implementations have different performance profiles when it comes to heavy math calculations, but the thing to focus on is the relative difference within each implementation.

Both JRuby and Rubinius were able to calculate the result significantly faster when using multiple threads. Contrast that with MRI. MRI’s multi-threaded results showed no significant improvement over its single-threaded result.

These figures are a direct representation of the GIL. The GIL in MRI prevented Ruby code from being executed in parallel, hence no speedup.

But…don’t they need a GIL?

At the end of the last chapter, I gave you three reasons why the GIL exists in MRI. This would be a good time to address how the other implementations solve these problems. Let’s take them in order.

  1. To protect internals from race conditions

    JRuby and Rubinius do indeed protect their internals from race conditions. But rather than wrapping a lock around the execution of all Ruby code, they protect their internal data structures with many fine-grained locks.

    Rubinius, for instance, replaced their GIL with about 50 fine-grained locks. Without a GIL, its internals are still protected, but because it spends less time in these locks, more of your Ruby code can run in parallel.

  2. To facilitate the C extension API

    JRuby has an easy answer to this: it doesn’t support the C extension API. For gems that need to run natively, they support Java-based extensions. I won’t go into any more detail about this.

    Rubinius does support the MRI C extension API. Interestingly, it simply preserves its GIL-less behaviour in the presence of these extensions. They opted to try to help gem authors fix thread-safety issues, rather than try to prevent them from happening. They say that no issues have cropped up thus far.

  3. To reduce the likelihood of race conditions in your Ruby code

    JRuby and Rubinius don’t have an answer to this one. Any thread-safety issues in your code are likely to crop up much more quickly when using these implementations. The simple reason is that your code will actually be running in parallel.

    The only way to solve this issue is to be aware of thread safety when writing your code. There’s lots more about this topic in the next few chapters.