Take the 2-minute tour ×

I'm seeing a weird effect in hotspot, and I wonder if anybody has experiencing the same thing before.

I'm computing the k nearest neighbors in a large data set, around 100k objects in 8 dimensions. I noticed a recent performance regression in my code, so I started trying to pin it down. Interestingly, the following seems to make the difference:

This version is ~25% slower (~812 vs. 644 seconds):

double distance = ...;
heap.add(distance, id);

compared to this version:

double distance = ...;
heap.add(new Double(distance), id);

where in add(Double, int) the Double value is actually used as double:

super.add(new DistancePair(distance /* double! */, id));

alternatively, I have also tried:

void add(Double dist, id) {
  this.add(dist.doubleValue(), id);

and even more interesting, this is also faster than calling the add(double,id) method directly! So extra method invocation + boxing = faster!?!

DistancePair is storing a primitive double and a primitive int. For reference, directly invoking the super add method is 773 seconds, so also slower than the Double wrapper.

(Sorry, I the full heap code is a bit too big for Stackoverflow. Esssentially, it is a size constrained heap, pretty standard stuff. It will store double-ID pairs, so it would need to unbox the Double anyway.)

I'm totally stunned by this. Why would adding extra boxing and unboxing increase the performance, in particularly when it is completely unnecessary. This shouldn't happen that adding an unnecessary object creation helps the hotspot compiler, it should be exactly the other way around.

I've tried a dozen variants of the code, and I can't get it to be as fast as with that unnecessary extra boxing. :-( Which sucks, because I want my code clean, and I don't want the next guy to do "oh, there is unnecessary boxing. Let's remove it" and see the same regression). Plus, with this kind of effects, you cannot rely on the next java version to have the same behavior... :-(

In general hotspot is doing a good job. The code is surpisingly fast, compared to other implementations. Judging from visualVM it has inlined some methods etc.

The experiments were run with OpenJDK 6 and OpenJDK 7, with some optimization flags enable and with defaults (except -Xshare:off) and show the same behavior. -Xint is now running. I'm also trying to make an isolated demo code to share here.

Any ideas of

  • why this is happening - when would hotspot be able to optimized boxing better than raw double
  • how to avoid it / help hotspot with proper optimization

Update: The -Xint runs, which obviously are a lot slower, seem to have the expected behavior: the double version is 35127, the Double version is 37001. But there is some variance, and I don't have many iterations yet.

Update: Also in bytecode the double version should clearly be faster than the other. So so far the best guess is that the hotspot compiler doesn't see the double variant as a bottleneck worth optimizing.

Update: With Java 8, the "nicer" code is finally as fast as the ugly code. But I still do not understand what is happening here. Anyway, if you are seeing a similar thing, try Java 8.

share|improve this question
Could it be that without the boxing, the JITter doesn't see it as a bottleneck and doesn't try to optimise it at all or not as hard? –  Daniel Fischer Sep 23 '12 at 15:30
Well, even if it recognizes the extra add(DoubleDistance, id) as bottleneck, the optimization would likely still need to call the same add(double, id) function after optimization. :-( –  Anony-Mousse Sep 23 '12 at 15:34
Notice that double takes 64 bit while object reference to DoubleDistance only 32 (on 32-bit JVM and 64 bit with compressed pointers). Allocation is cheap, maybe smaller amount of data on stack is improving performance? –  Tomasz Nurkiewicz Sep 23 '12 at 15:36
I'm using Linux, there is no client VM for Linux/amd64. My benchmark JVM (that I use for regression testing) settings are: -Xshare:off -XX:+UseFastAccessorMethods -XX:CompileThreshold=1000 -XX:+AggressiveOpts. I havn't benchmarked with default settings yet, I will do so later. I will also try openjdk 7, because apparently the benchmarking system is running openjdk 6. Sharing the full code is messy because of all the dependencies. The heap is actually 4 times subclassed adding the functionality needed. But in particular, the faster "add" method is really only doing unboxing. –  Anony-Mousse Sep 24 '12 at 7:48
Can you paste the compiled bytecode snippets for each method you are benchmarking? Also, If you run in interpreted mode using '-Xint', do you see the same result? These two pieces of information will either point towards the Java source to bytecode compiler or the JIT. –  James Branigan Sep 24 '12 at 8:46

2 Answers 2

The code

heap.add(distance, id);

actually compiles to

heap.add(Double.valueOf(distance), id);

where Double.valueOf() is:

public static Double valueOf(double d) {
    return new Double(d);

Since you do only method calls where (-> no other expensive operations), the cost of the method calls is important. I'm a bit surprised, though, that JIT can't optimize the method call away since Double is final.

The second effect is that you can't access the value directly anymore. I'm pretty sure the JIT can't optimize the indirection away so every time you use the value, an additional pointer de-reference will happen.

This is one of the limitations of Java's autoboxing. I don't know a solution which will magically fix this. You will probably have to write new Double() in a few places to preserve performance or change the API to work with double primitives.

Another solution is to always use the type Double so you rarely have to convert (and most conversions will be downcasts to double which are pretty fast). But that will only save the upcast; you still need to create thousands of Double instances (they aren't cached) and you will pay for the indirect value access each time you use it.

share|improve this answer
There is an add(double, id) method, too. There is no auto-boxing happening here. Profiling shows this, there are no Double object allocated in a full hprof run. –  Anony-Mousse Sep 24 '12 at 12:48
Please try to achieve the same effect with a minimal example (just the 2-5 methods involved). I think something else happens here. –  Aaron Digulla Sep 24 '12 at 12:52
So far my isolated test code does not show this effect. Obviously it has a much less deep call stack. –  Anony-Mousse Sep 24 '12 at 16:10
In that case, the code inside of add(double) and add(Double) must be different. What happens if you move the common code into a new method? –  Aaron Digulla Sep 24 '12 at 16:13
The code in add(Double) just calls add(double) - that is where the common code already is... –  Anony-Mousse Sep 24 '12 at 16:17
up vote 0 down vote accepted


After a lot of debugging, I found out that it wasn't just the boxing.

I tried a lot of things, all the way to having the JVM dump the generated assembler code, and studying this code. And I couldn't find the root cause of the problem. Maybe the extra boxing caused the hotspot to optimize the code better, wrap it differently, or perform some other locking/synchronization/inlining/whatever.

However, it seems that it only occurs with

  • Some revisions of the code, but not the earlier ones (with more boxing); but I could not pinpoint it to a tiny enough change even with bisecting.
  • Java 7, but not Java 8 runtime
  • One particular CPU, but not any of the other CPUs that I tested

In particular the last two made me choose the following solution:

  • Use the nicer, newer code (that was also debugged in detail)
  • On that particular system (CPU), use Java 8

Now that Java 8 is released, this is a very reasonable approach. In particular, as it only affects one particular system, so I do not need to upgrade to Java 8 everywhere, unless I suspect a similar slowdown.

share|improve this answer

Your Answer


