Tuesday, August 2, 2011

JRuby and Java 7: What to Expect

Java 7 has landed, with a modest set of new features and a few major improvements as well. What can you expect from JRuby running on Java 7?

What's In Java 7


The biggest changes in Java 7 are not related to the Java language at all. Sure, there's the "project coin" enhancements to the Java language, which add some exception-handling shortcuts, new literals for numbers, arrays, hashes, the oft-requested "strings in switch" support, and a few other things. But they're modest incremental changes; the real revolution is at the JVM and JDK level.

Invokedynamic


The most important change in Java 7 is the incorporation of a new bytecode -- invokedynamic -- and an API for building chains of "method handles" to back that bytecode up.

You can look at invokedynamic as a way for JVM users to communicate directly with the optimizing backend of the JVM. Method handles act as both function pointers and as function combinators, allowing a built-in way to construct a call protocol flow from a caller to a callee. You can move arguments around, insert new arguments, process existing arguments and return values, catch exceptions, and perform fast guarded branches between two (or more) paths. The invokedynamic bytecode itself provides a bytecode-level hook to which you attach your method handle chain, with the assumption that the JVM can optimize that chain directly into the invokedynamic caller.

The tl;dr is that invokedynamic makes it possible for the JVM to see through complicated method call logic, such as that found in dynamic languages, and optimize that logic like it would for regular "static" calls.

JRuby's master branch already takes heavy advantage of invokedynamic, by routing most Ruby calls through invokedynamic operations. For simple paths and those that have been optimized by the Hotspot guys (Hotspot is the VM at the core of OpenJDK), invokedynamic often provides performance improvements of 150-200%, with work ongoing to make it even faster. Other paths may not be as well-optimized by the "dot zero" version of OpenJDK 7, so there's opportunity to improve them.

Because JRuby is already well along the road to utilizing invokedynamic, you can try it out today.

  1. Build your own JRuby from master or grab a snapshot from our CI server.
  2. Grab a build of OpenJDK 7 from Oracle (or a build of OpenJDK 7 for OS X).
  3. Point JAVA_HOME at the new JDK and try out JRuby!
We're looking for small benchmarks that show the performance of invokedynamic (good or bad), so please contact me, the JRuby team, or the JRuby users mailing list with your reports from the field. Also, feel free to open performance bugs on the JRuby bug tracker if invokedynamic performs worse than non-invokedynamic. Pass -Xcompile.invokedynamic=false to JRuby to revert to the old non-invokedynamic logic.

NIO.2

NIO is Java's "New IO" APIs, a set of wrappers around low-level file-descriptor logic and memory buffers. NIO has been around since Java 1.4, but the recent update -- dubbed NIO.2 -- brings a sorely-needed update to the functionality provided:
  • Filesystem operations (like symlinks, permissions, etc) are now almost all available through NIO.2's filesystem APIs. This also includes standard, cross-platform support for filesystem events, such as watching a directory for changes (using efficient OS-level operations, rather than polling).
  • File and directory walking now comes with considerably less overhead and more options for filtering directory lists before handing filenames off to user code. There's also support for opening a directory directly and walking its contents as you would a file.
  • Most IO channel types now have asynchronous versions. Asynchronous in this case means "punt my IO operation to a built-in thread pool", with subsequent code checking on the status of those operations and getting results from a "future" handle.
For JRuby, the new IO APIs will mean we can support more filesystem operations across platforms without resorting to native code. It will also provide JRuby users a means of handling filesystem events and asynchronous IO operations without using a platform-specific library. We have not yet started adding NIO.2 support to JRuby's core classes, but that will come soon.

General Improvements

There's lots of smaller, less flashy changes in OpenJDK that also appear to help JRuby.

Even without invokedynamic, the latest OpenJDK 7 builds usually perform better than OpenJDK 6. Some benchmarks have proven to be as much as 2x faster, just by upgrading the JVM! General perf improvements will be more modest, but in almost every case we've tested OpenJDK 7 definitely performs better.

The release of OpenJDK 7 also brings improvements to the "tiered" compilation mode. Tiered compilation aims to merge the benefits of the "client" mode (fast startup) with those of the "server" mode (maximum peak performance). You can turn on tiered compilation using -XX:+TieredCompilation (in JAVA_OPTS or at the "java" command line, or prefixed with -J when passed to JRuby). We're looking for user reports about how well "tiered" mode works, too.

This general improvement means that even JRuby 1.6.x users can take advantage of OpenJDK 7 today, with the promise of even bigger improvements in JRuby 1.7 (our target release for pervasive invokedynamic support).

Consistency

As with previous Java releases, a great deal of care has been taken to ensure existing applications work properly. That applies as well to Java 7. We have been testing against Java 7 for over a year, on and off, and recently started running tests "green" with even heavy invokedynamic use.

We have made no major Java 7-specific fixes in JRuby...it should generally "just work".

Let Us Know!

As always, we really want to hear from you bleeding-edge users that are playing around with JRuby on Java 7. Please don't be shy...let us know how it works for you!

Update: The Hotspot guys have been helping me find invokedynamic bottlenecks in a few JRuby microbenchmarks, and discovered that a flaw in invokedynamic was causing too much code to inline, forcing out more important optimizations. The details belong in another post, but they offered me a long Hotspot flag to accomplish basically what their fix does: -XX:CompileCommand=dontinline,org.jruby.runtime.invokedynamic.InvokeDynamicSupport::invocationFallback ... With this flag, performance on e.g. "tak" easily beats stock JRuby (see the third benchmark run here: https://gist.github.com/1121880).

I would recommend trying this flag if you are finding invokedynamic slowdowns in JRuby.

18 comments:

  1. What do you mean by "new literals for [...] arrays, hashes"? "Language support for collections" is not in Java 7, or am I missing something?

    ReplyDelete
  2. Robin: Right you are...I was mistaken. I thought the literal array and hash syntaxes made it, but they must have been cut after I last checked.

    ReplyDelete
  3. Nice! When is jRuby 1.7 expected to be released ?

    ReplyDelete
  4. Anonymous: In order to let the Hotspot guys have a fair chance at optimizing invokedynamic, we've tentatively delayed JRuby 1.7 until there's an update release to OpenJDK 7. That should bring us closer to true invokedynamic performance, and provide a better picture of what it can do for a language like Ruby.

    ReplyDelete
  5. I think invoke dynamic and method handles are great for dynamic language implementors and will benefit languages like JRuby and Groovy but should those who are using Java the language be excited about this? If so why? What are the immediate benefits from using Java 7 today for people who are using the Java language? For those of us who can see through the Oracle marketing hype it’s a pretty underwhelming release (for the Java language) for something that has been 5 years in the making! Ask yourself how far something like JRuby or Scala has come in 5 years and I am sure you’ll agree.

    There is a lot of R&D work going into a bunch of stuff that doesn't really have immediate effects on Java the language. One could argue that Java the language would have been in a far better situation today if resources and effort were put into enhancing/fixing the Java language instead of elsewhere.

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. A response to the previous commenter. Oracle's main achievement regarding Java 7 was actually getting the release out. The situation they inherited from Sun was pretty messy. Judge them on how they handle Java 8.

    Best,
    Ismael

    ReplyDelete
  8. Whatever what people think about Java, the JVM is a really good piece of software, and JRuby gives to us the best of both worlds: the awesome Ruby ecosystem and the solid JVM.

    Thanks you JRuby guys, you r0cks!

    ReplyDelete
  9. Anonymous #2: What Oracle did with Java 7 and Java 8 was split up into two releases what would have taken too long as one. Invokedynamic may have profound implications for the future of the Java language, since now there's a simple way to add in new dispatch mechanisms without performance penalty. If we had waited and Oracle had tried to do everything in Java 8, we wouldn't have Java 7 today. Also, invokedynamic comes with the Method Handle API, which provides normal Java code with a far more flexible and performant alternative to reflection that was possible before. Invokedynamic as a bytecode isn't directly accessible to Java, this is true. But it might be useful for future Java language changes, it's useful to other languages *today*, and the other parts of the invokedynamic JSR (method handles) are useful to Java developers right now.

    ReplyDelete
  10. Any real benchmarks, like the ruby benchmark suite?
    -roger-

    ReplyDelete
  11. Hi Charles,

    awesome work. Looking forward to JRuby 1.7.

    Do you know if there are planes (or available libs) to implement parallel collections in JRuby similar to Scala 2.9. There parallel collections are awesome.

    Code like this:
    val l = List.range(1,10000)
    val (even, uneven) = l.par.partition(_ % 2 == 0)

    can be run be split over the available CPUs and dramatically increase performance (when the problem justifies it).

    Given that JRuby 1.7 works well with Java 7 and there is fork/join now, I thought that this might be possible in JRuby too without using fork/join myself but as a library.

    If you know of anything like, please let me know.

    Markus

    ReplyDelete
  12. @Markus you can use one of the several pure ruby parallel libraries out there (albeit using threads, and who knows how much you'll really save...).

    ReplyDelete
  13. Geez, I think I'm starting to fall in love with Java 7 + JRuby 1.6.3. It's actually FAST!

    REE 1.8.7:

    arturas@arturaz-fujitsu:~/work/spacegame/server$ time spec spec/server/dispatcher_spec.rb
    .................................

    Finished in 1.422324 seconds

    33 examples, 0 failures

    real 0m10.493s
    user 0m3.468s
    sys 0m0.388s

    JRuby 1.6.3:

    $ JAVA_OPTS="-XX:+TieredCompilation" jruby --ng-server

    arturas@arturaz-fujitsu:~/work/spacegame/server$ time jruby --ng -S spec spec/server/dispatcher_spec.rb
    .................................

    Finished in 1.534 seconds

    33 examples, 0 failures

    real 0m13.723s

    I'd say that is nice. Looking forward to invokedynamic stuff ;)

    ReplyDelete
  14. By the way - if you run jruby with --ng, then it uses all the JVM options on ng server jvm, right?

    ReplyDelete
  15. Markus: I would love to see such support in Ruby, or using available Java libraries. I'm also curious if it's possible to use those Scala collections from Ruby; there's some disconnect between Ruby and Scala (or between Scala and *anything* really) but perhaps we could find a way.

    We JRuby guys have no current plans to implement concurrent/parallel collections on our own. I believe there's others in the Ruby world who would be better-qualified to do that. Perhaps you could take what the Scala collections do and make a Java or Ruby version?

    Artūras: Glad to hear your perf (startup mostly?) is looking good! invokedynamic may or may not help startup, but it will definitely help straight-line optimized performance. You can certainly play with JRuby master + Java 7 and see for yourself too :)

    ReplyDelete
  16. Well, after a lot of tinkering, it seems that running JRuby in client mode (13s) vs tiered more (17s) still gives best performance for specs.

    After we'll deploy our new version using jruby I'll try to gather some data if tiered mode is better than pure server mode.

    And last time I tinkered with jruby-head it crashed somewhere in the activerecord :))

    ReplyDelete
  17. arturas@arturaz-fujitsu:~/work/spacegame/server$ jruby -S gem install activesupport --no-rdoc --no-ri --version ">=3.0.5"
    ERROR: While executing gem ... (TypeError)
    can't convert nil into String
    arturas@arturaz-fujitsu:~/work/spacegame/server$ jruby -v
    jruby 1.7.0.dev (ruby-1.8.7-p330) (2011-08-08 dd6e865) (Java HotSpot(TM) Client VM 1.7.0) [linux-i386-java]


    yeah... :))

    ReplyDelete
  18. ArgumentError: Valid types are [:development, :runtime],
    initialize at /home/arturas/.rvm/rubies/jruby-head/lib/ruby/site_ruby/1.8/rubygems/dependency.rb:44
    find_name at /home/arturas/.rvm/rubies/jruby-head/lib/ruby/site_ruby/1.8/rubygems/source_index.rb:242
    activate at /home/arturas/.rvm/rubies/jruby-head/lib/ruby/site_ruby/1.8/rubygems.rb:254
    gem at /home/arturas/.rvm/rubies/jruby-head/lib/ruby/site_ruby/1.8/rubygems.rb:1215
    (root) at /home/arturas/.rvm/gems/jruby-1.6.3/bin/spec:18


    This is fun too.

    JRuby misses on:

    raise "bla bla bla"
    + "bla bla bla"

    + gets ignored somehow. Doesn't look like valid syntax without \ for me too, perhaps we need a report to rubygems?

    ReplyDelete