Tuesday, February 5, 2013

Multi-threading in JRuby

The traditional Ruby you might be used to running (along with other interpreted languages such as python) make use of a global interpreter lock which ensures that only one thread can interpret code at any given time. In Ruby 1.8, the process is only allocated one OS thread meaning any attempts at parallelism are basically useless. In Ruby 1.9 things were improved slightly; the GIL still only runs on one thread, but the process can take advantage of multiple threads. This means that anything I/O bound can run concurrently (database queries, network requests) but you won't be able to make effective use of the processing power of all of your cores.

JRuby on the other hand, is built upon the JVM. One can make use of "true" native threads which are able to run concurrently with you parent thread.

To visualize how this works check out this diagram:


Read more about the GIL here (from which I borrowed the above diagram).

Setup

Here are some quick instructions on how to setup JRuby on your machine so we can test out multi-threaded performance. The easiest way to install JRuby in my opinion is through Ruby Version Manager. RVM is essentially an easy way to manage multiple ruby versions (and their associated gems) on a single machine. For Linux/Unix/OSX instructions, see here. For windows go here.

1- Install JRuby:

>rvm install jruby

2- Check that it's installed

> rvm list gemsets









3- rvm use <name of the jruby gemset>
> rvm use jruby-1.7.1

4- Test it out in irb (ruby's interactive console). You should see "java" as the
RUBY_PLATFORM if things are installed properly:






Sample App

The following are two sample apps which showcase multi-threading in JRuby vs. Ruby 1.9 vs Ruby Enterprise Edition 1.8.7.

JRuby Example (also showcasing how easy it is to call java code from Ruby):

Standard Ruby example:
To run the experiment:
1- Run in Jruby:
> ruby multi_thread_jruby.rb

2- Run in Ruby 1.9.3
> rvm install ruby-1.9.3-p286
> rvm use ruby-1.9.3-p286
> ruby multi_thread_ruby.rb

3- Run in Ruby 1.8.7
> rvm install ree-1.8.7-2012.02
> rvm use ree-1.8.7-2012.02
> ruby multi_thread_ruby.rb

Average completion time summary:
JRuby(1.7.1):            199.3ms
Ruby(1.9.3p286):         610.0ms
Ruby(ree-1.8.7-2012.02): 748.6ms

As we can see, JRuby is close to 4 times faster than Ruby 1.8 on my 4 core MacBook Air. Ruby 1.9 performs slightly better by being able to take advantage of time during which the process is I/O blocked. Overall, JRuby is the clear choice for maximizing multi-threaded performance.

Additional Reading: 


More on calling Java from JRuby:
https://github.com/jruby/jruby/wiki/CallingJavaFromJRuby

An interesting look at benchmarks of JRuby compared to other ruby versions. This shows that JRuby is not better in all scenarios: 
http://etehtsea.me/the-great-ruby-shootout

#SeekDiversePerspectives

7 comments:

  1. Interesting results. On my dual core (4 thread) Samsung Series 9 running Ubuntu 12.10 I got the following results (didn't test REE, don't care)

    [multi_thread_ruby.rb]
    MRI 1.9.3-p248 : 322.8ms
    JRuby 1.7.2 : 214.5ms
    Rubinius 2.0.0.rc1 : 144.9ms
    MagLev 1.1RC1 : 244.6ms

    [multi_thread_jruby.rb]
    JRuby 1.7.2 : 232.6ms

    I would think JRuby would perform a bit better. I was pleasantly surprised by how well Rubinius performed.

    ReplyDelete
  2. @Jesse,

    If you change this benchmark to use floats you will see Rubinius is about where JRuby is. For this simple loop tagged Fixnums is helping both rbx and MRI (JRuby has to box fixnums in a full Ruby object).

    The fact that they can do extra optimizations we currently cannot on Fixnums is not bad, but it is not helpful in showing the speed differences of using multiple cores with native threads. Since both JRuby and Rubinius are multi-threaded you should expect them to benefit from have more cores to do work. For MRI you should expect a linear slowdown.

    ReplyDelete
  3. I would have thought that the HotSpot compiler might remove the count loop altogether since it doesn't output anything. But apparently it only JITs whole methods once their call threshold is reached, not blocks.

    Anyway, thanks for explaining how to use the Java concurrency classes from JRuby!

    ReplyDelete
  4. > As we can see, JRuby is close to 4 times faster

    Actually, java optimizer will skip this block as it affects nothing:

    count = 0
    1000000.times do
    count += 1

    So your performance test counts an air for JRuby in this example. That is a well-known mistake of novice peromance measurers, who claims Java is ultra-fast using dummy cycles which Java just ignores.

    ReplyDelete
    Replies
    1. Thanks for the feedback Oleg. Can you suggest a test that wouldn't count for air?

      Delete
    2. shouldn't ignore it as it affects count.

      Delete
  5. For your JRuby example, if you run it with num_threads = 1, you will notice that multi-threading doesn't seem to work properly in JRuby.

    ReplyDelete