3

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      
Water Guo
  • 79
  • 5

2 Answers2

2

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.

Tagir Valeev
  • 87,515
  • 18
  • 194
  • 305
1

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.

Philippe Marschall
  • 4,277
  • 1
  • 27
  • 48