Java BigDecimal strange performance behavior
I ran into unusual behavior with BigDecimal today. In a simple word, there is a significant difference between the following two pieces of code trying to do the same.
int hash = foo();
BigDecimal number = new BigDecimal(hash);
against
BigDecimal number = new BigDecimal(foo());
to prove it I have the class below to show the difference. My java is 1.7.0_75-b13, 64 bit, mac. In my environment, the first loop took 2 seconds, the second loop took 5 seconds.
import java.math.BigDecimal;
public class Crazy {
public static void main(String[] args) {
new Crazy().run();
}
void run() {
// init
long count = 1000000000l;
// start test 1
long start = System.currentTimeMillis();
long sum = 0;
for (long i=0; i<count; i++) {
sum = add(sum);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
// start test 2
long start2 = end;
sum = 0;
for (long i=0; i<count; i++) {
sum = add1(sum);
}
long end2 = System.currentTimeMillis();
System.out.println(end2 - start2);
}
long add(long sum) {
int hash = hashCode();
BigDecimal number = new BigDecimal(hash);
sum += number.longValue();
return sum;
}
long add1(long sum) {
BigDecimal number = new BigDecimal(hashCode());
sum += number.longValue();
return sum;
}
}
javap output
long add(long);
Code:
0: aload_0
1: invokevirtual #56 // Method java/lang/Object.hashCode:()I
4: istore_3
5: new #60 // class java/math/BigDecimal
8: dup
9: iload_3
10: invokespecial #62 // Method java/math/BigDecimal."<init>":(I)V
13: astore 4
15: lload_1
16: aload 4
18: invokevirtual #65 // Method java/math/BigDecimal.longValue:()J
21: ladd
22: lstore_1
23: lload_1
24: lreturn
long add1(long);
Code:
0: new #60 // class java/math/BigDecimal
3: dup
4: aload_0
5: invokevirtual #56 // Method java/lang/Object.hashCode:()I
8: invokespecial #62 // Method java/math/BigDecimal."<init>":(I)V
11: astore_3
12: lload_1
13: aload_3
14: invokevirtual #65 // Method java/math/BigDecimal.longValue:()J
17: ladd
18: lstore_1
19: lload_1
20: lreturn
source to share
I cannot reproduce this. Consider the following Microbenchmark:
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class BigDecimalBenchmark {
static int i = 1024;
@Benchmark
public BigDecimal constructor() {
return new BigDecimal(foo());
}
@Benchmark
public BigDecimal localVariable() {
int hash = foo();
return new BigDecimal(hash);
}
private static int foo() {
return i;
}
}
Which gives the following output:
Benchmark Mode Samples Score Error Units
BigDecimalBenchmark.constructor thrpt 100 180368.227 Β± 4280.269 ops/ms
BigDecimalBenchmark.localVariable thrpt 100 173519.036 Β± 868.547 ops/ms
Update
Edited test to make foo () not inline.
source to share
I have reproduced the effect in Java 1.7.0.79 using the following test:
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.annotations.*;
@Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 3, timeUnit = TimeUnit.SECONDS)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(2)
@State(Scope.Benchmark)
public class AddTest {
long add(long sum) {
int hash = hashCode();
BigDecimal number = new BigDecimal(hash);
sum += number.longValue();
return sum;
}
long add1(long sum) {
BigDecimal number = new BigDecimal(hashCode());
sum += number.longValue();
return sum;
}
@Benchmark
public void testAdd(Blackhole bh) {
long count = 100000000l;
long sum = 0;
for (long i=0; i<count; i++) {
sum = add(sum);
}
bh.consume(sum);
}
@Benchmark
public void testAdd1(Blackhole bh) {
long count = 100000000l;
long sum = 0;
for (long i=0; i<count; i++) {
sum = add1(sum);
}
bh.consume(sum);
}
}
The results are as follows:
# JMH 1.9 (released 40 days ago)
# VM invoker: C:\Program Files\Java\jdk1.7.0_79\jre\bin\java.exe
# VM options: <none>
Benchmark Mode Cnt Score Error Units
AddTest.testAdd avgt 20 214.740 Β± 4.323 ms/op
AddTest.testAdd1 avgt 20 1138.269 Β± 32.062 ms/op
It's funny that using 1.8.0.25 the results are exactly the opposite:
# JMH 1.9 (released 40 days ago)
# VM invoker: C:\Program Files\Java\jdk1.8.0_25\jre\bin\java.exe
# VM options: <none>
Benchmark Mode Cnt Score Error Units
AddTest.testAdd avgt 20 1126.126 Β± 22.120 ms/op
AddTest.testAdd1 avgt 20 217.145 Β± 1.905 ms/op
However, on 1.8.0_40 both versions are fast:
# JMH 1.9 (released 40 days ago)
# VM invoker: C:\Program Files\Java\jdk1.8.0_40\jre\bin\java.exe
# VM options: <none>
Benchmark Mode Cnt Score Error Units
AddTest.testAdd avgt 20 218.925 Β± 5.093 ms/op
AddTest.testAdd1 avgt 20 217.066 Β± 1.427 ms/op
In all these cases, the methods of the method add
and add1
are built into the method of the caller. It looks like this is only due to internal changes to the loop unrolling mechanism in the JIT compiler: sometimes your loop unfolds well, sometimes it doesn't.
source to share