Tuesday, January 30, 2007

Improving Java Integration Performance

I created JRUBY-501 to track performance improvements to Java integration, since it's come to light recently that it may be one of our biggest bottlenecks now. And I found a ripe, juicy fix already.

For every call to a Java type, we call JavaUtilities.matching_method with a list of potential methods and the given argument list. matching_method compares the available methods and the types of the arguments, choosing the best option and returning it to be called. This is essentially our heuristic for choosing an overloaded method from many options, given a set of arguments.

Problem was, we didn't cache anything.

Given a list of argument types and a list of methods, there's only ever going to be one appropriate choice. Unfortunately our code was doing the search for every single call, and you can imagine how much additional overhead that added. Or perhaps you can't, and I'll show you.

Here's the numbers before my tiny change:

 38.862000   0.000000  38.862000 ( 38.861000)
40.230000 0.000000 40.230000 ( 40.230000)
This test basically just instantiates a StringBuffer and appends the same character to it 100_000 times. It takes roughly 40 seconds to do that with the old code.

And here's with my changes:
  3.295000   0.000000   3.295000 (  3.294000)
2.933000 0.000000 2.933000 ( 2.933000)
Yes, you're reading that right. It's an 13 times improvement.

And the change was trivial: given the list of methods and argument types, cache the correct method. So simple, so elegant, so effective.

So does this affect regular Ruby code? You better believe it does!

I had been intrigued by the fact that some of the first methods JITed during rdoc generation were all JavaSupport methods. That told me something in rdoc was using a class we provide through Java integration, rather than natively or in pure Ruby. So I figured with this change, I'd re-run the numbers.

Before the change, a full rake install with rdoc took about 42s, or about 31s with ObjectSpace disabled. And now, the "after" numbers:
with ObjectSpace:
real 0m29.765s
user 0m28.843s
sys 0m2.169s

without ObjectSpace:
real 0m24.984s
user 0m23.559s
sys 0m1.757s
This is by far the largest increase we've seen in rdoc performance in several months. The fix should also drastically improve the performance of libraries like ActiveRecord-JDBC, which is extremely Java-integration-heavy.

Another area that's been painful was installing Rails with all docs. It used to take over an hour, but now it's under *seven minutes*.

I hope those of you who've seen or blogged about performance problems (especially with the aforementioned ActiveRecord-JDBC) will try re-running your tests. This improvement ought to have a very noticeable effect on benchmarks.

Now the only concern I have with the caching is that it's a little coarse; there may be better places to do the caching, or finer-grained items to cache against. And we could probably pre-fill the cache with some likely candidates. But an improvement like this outweighs those concerns, so it's been committed...and there's bound to be similar improvements as well.

Boy oh boy is that low-hanging fruit looking ripe.


Tom Palmer said...

Thanks much for working on this. I've been a bit out of JRuby for a while, but I haven't been completely away. This is good news.

mortench said...

What about cache invalidation?

kasou said...

What version of JRuby should I grab?
I tried to compile current trunk, but it doesn't run any Ruby code at the moment.
Everything breaks down with similar exceptions:

org.jruby.exceptions.RaiseException: IO error -- java
Caused by: java.lang.ClassCastException: org.jruby.ast.IterNode cannot be cast to org.jruby.ast.LocalAsgnNode
at org.jruby.evaluator.EvaluationState.evalInternal(EvaluationState.java:933)
at org.jruby.evaluator.EvaluationState.evalInternal(EvaluationState.java:278)
at org.jruby.evaluator.EvaluationState.eval(EvaluationState.java:156)
at org.jruby.evaluator.EvaluationState.evalInternal(EvaluationState.java:1291)
at org.jruby.evaluator.EvaluationState.eval(EvaluationState.java:156)
at org.jruby.RubyObject.eval(RubyObject.java:511)
at org.jruby.Ruby.loadNode(Ruby.java:1186)
at org.jruby.util.BuiltinScript.load(BuiltinScript.java:54)
at org.jruby.Ruby$1.load(Ruby.java:577)
at org.jruby.runtime.load.LoadService.smartLoad(LoadService.java:266)
at org.jruby.runtime.load.LoadService.require(LoadService.java:285)
at org.jruby.javasupport.JavaEmbedUtils.initialize(JavaEmbedUtils.java:55)
at sk.h2.tutorial.sokoban.ruby.RubySupport.setup(RubySupport.java:22)
at sk.h2.tutorial.sokoban.Main.main(Main.java:14)
IO error -- java

Anonymous said...

Can JRuby cause runtime error exceptions in JDBC