Reputation: 79
Today I came cross a strange performance behaviour with BigDecimal. In a simple word, there is a significant difference between the following two pieces of code trying to do the same thing
int hash = foo();
BigDecimal number = new BigDecimal(hash);
vs
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 2s, second loop took 5s.
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
Upvotes: 3
Views: 257
Reputation: 4604
I can not 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 the benchmark to make foo() not inlineable.
Upvotes: 1
Reputation: 100279
I reproduced the effect on Java 1.7.0.79 using the following benchmark:
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 the following:
# 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
The amusing thing is that using 1.8.0.25 the results are strictly 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 of these cases add
and add1
methods are inlined into the caller method. Seems that it's just related to internal changes with loop unrolling mechanism in JIT compiler: sometimes your loop is nicely unrolled, sometimes it's not.
Upvotes: 2