Virtually free - JVM callsite optimization by example
14 Dec 2015Recently, this post by Swift creator Chris Lattner has been making the rounds, which does a nice job of detailing method dispatch in various languages. I thought I would add some color on how the Hotspot JIT can safely turn a virtual call into a static one.
Let say we have a simple class Foo
And another class Bar
which extends Foo
and implements its own function
Now we’ll create a method that calls Foo.function
, and then give that method a workout:
We can run this via:
1. As always, if you want to follow along you’ll need to follow these steps to be able to see assembly on your JVM
The first c2 compile of call
looks like this:
Notice that it has simply inlined Foo.function
. At first glance, this seems odd, as this code is not correct if passed a Bar
, which is valid input. Whats going on?
The trick here is that Hotspot doesn’t know about the contents of Bar
yet, because classes are loaded lazily. When it came time to compile call
it saw that there was no subclass implementation overriding Foo.function
, so it just made the virtual call static (and subsequently inlined it). This is known as Class Hierarchy Analysis (CHA), and is an important optimization to clean up Java’s virtual by default model.
This only works, however, if there is some way of invalidating the method if this assumption not longer becomes true (eg this example). This is handled in doCall.cpp:
When we get to loading Bar
, our compile of call
is discarded due to this failed dependency. If you have a debug jvm, you can see this happening by running with -XX:+TraceDependencies
Failed dependency of type unique_concrete_method
context = Foo
method = {method} {0x7f04a0f1fc58} 'function' '()I' in public synchronized 'Foo'
witness = Bar
code: nmethod 760 36 4 CallTest::call (5 bytes)
Marked for deoptimization
context = Foo
dependee = Bar
context supers = 2, interfaces = 0
Compiled method (c2) 760 36 4 CallTest::call (5 bytes)
(...)
Dependencies:
Dependency of type unique_concrete_method
context = Foo
method = {method} {0x7f04a0f1fc58} 'function' '()I' in public synchronized 'Foo'
[nmethod<=klass]Foo
Failed dependency of type leaf_type
context = Foo
witness = Bar
code: nmethod 760 28 2 CallTest::call (5 bytes)
Now when Hotspot gets around to compiling call
again in the second loop, it will correct this:
This verison looks similar, but there now is a class guard around the inlining of Foo.function
. The other branch, which I’ve ommitted here, jumps back to the interpreter. Note that although this works for both classes, the profiled type information has led Hotspot to only optimize this for Foo
.
If you’re interested in learning more, I highly recommend Alexy Shipilev’s epic rundown of Java method dispatch, which covers this and more in detail.
(Standard self-plug: If you enjoy these sorts of random details, follow me on twitter)