25 July, 2011

java.math.BigDecimal performance

These days I had to do some monetary calculations in Java. This reminded me again about primitive double type and that it is absolutely the worst thing you can use when dealing with money. As you know, primitive double don't necessarily give you the right value, only the value that can be stored in a binary number format (any decimal number is represented by an approximated value). Whilst, java.math.BigDecimal gives you the required precision, its performance is very poor comparable to primitive types.

Below are few simple tests that are measuring BigDecimal performance based on a simple computation x2 + y2 which is performed in a loop 250000 times. In order to exclude the JVM warm-up cost, each test was executed 10 times. The execution time was calculated as average of all executions except first execution (exec2 + exec3 + … + exec10) / 9.

  1. Test BigDecimal computations (execution average time: ~ 767 ms)
    BigDecimal bd = null;
    for (double x = 0.5; x <= 500; x += 0.5) {
        for (double y = 0.5; y <= 500; y += 0.5) {
          bd = BigDecimal.valueOf(x).pow(2).add(BigDecimal.valueOf(y).pow(2));
        }
    }
    
    We got around 0.003 ms per computation. Not bad, but wait to see how fast primitive double is. Next test will show us how much time we spend on creation of BigDecimal objects.
  2. Test BigDecimal constructions (execution average time: ~ 468 ms)
    for (double x = 0.5; x <= 500; x += 0.5) {
      for (double y = 0.5; y <= 500; y += 0.5) {
        BigDecimal.valueOf(x);
        BigDecimal.valueOf(y);
      }
    }
    
    As you can see, only construction of initial operands (x and y) took around 61% of computation time. We can assume that rest 39% are also spent on BigDecimal object creations (results of pow() and add() operations). Knowing that BigDecimal objects are immutable, let’s see how much we can gain by caching BigDecimal instances.
  3. Test BigDecimal computations with cache (execution average time: ~ 309 ms)
    BigDecimal bd = null;
    for (double x = 0.5; x <= 500; x += 0.5) {
      for (double y = 0.5; y <= 500; y += 0.5) {
        BigDecimal bigDecimal1 = cache.get(x);
        BigDecimal bigDecimal2 = cache.get(y);
        bd = bigDecimal1.pow(2).add(bigDecimal2.pow(2));
      }
    }
    
    Cache was prepared as follows:
    Map<Double, BigDecimal> cache = new HashMap<Double, BigDecimal>();
    for (double x = 0.5; x <= 500; x += 0.5) {
      cache.put(x, BigDecimal.valueOf(x));
    }
    
    As you can see, caching BigDecimal gives a performance boost. Now it’s time to see how the primitive double type is performing.
  4. Test double computations (execution average time: ~ 2 ms)
    double d = 0.0;
    for (double x = 0.5; x <= 500; x += 0.5) {
      for (double y = 0.5; y <= 500; y += 0.5) {
        d = x * x + y * y;
      }
    }
    
    Computation speed is great but unfortunately floating point numbers cannot be used for monetary calculations.

Performance Improvement to BigDecimal

Java SE 6 Update 25 made some performance improvements for BigDecimal. Following is the excerpt from Java SE 6 Update 25 Release Notes:

Improvements have been made to class BigDecimal enhancing its performance by thirty percent. BigDecimal is enabled by specifying -XX:+AggressiveOpts command option.

I repeated all the above tests with -XX:+AggressiveOpts option. Below is the table with results:

TestAverage Exec TimePerformance Boost
Normal-XX:+AggressiveOpts
BigDecimal computations767 ms644 ms16%
BigDecimal constructions468 ms394 ms16%
BigDecimal computations with cache309 ms309 ms0%
double computations2 ms--

Results speak themselves, -XX:+AggressiveOpts option gave us 16% performance improvement. It’s not a 30% improvement but still it is something, if we take into account that we just added a JVM option. The most interesting thing is that we got 0% performance improvement for the test which uses cache and the performance improvement of BigDecimal computations test and BigDecimal constructions test are the same. This leads me to think that improvements were made mostly for BigDecimal objects creation.

Regarding BigDecimal I would like also to remind you about:

  • Never use BigDecimal(double) constructor. The recommended constructor is BigDecimal(String), or in case you want to convert an existing double into a BigDecimal you can use BigDecimal.valueOf(double) static method.
  • When dividing always specify a RoundingMode. Instead of divide(BigDecimal) use divide(BigDecimal, RoundingMode). This has to be done in order to avoid ArithmeticException. Below is an excerpt from BigDecimal javadoc:
    In the case of divide, the exact quotient could have an infinitely long decimal expansion; for example, 1 divided by 3. If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown.

At the end, I want to say that for my task I used neither double nor BigDecimal. I used primitive long type and I stored money as cents, pennies (or the equivalent, of course). The choice was done because of performance reasons. I had to do many very simple computations but very often.

Update (05-Aug-2011): Be aware of bug, recently discovered in Java 7 which is also applicable for Java 6 in case when the following JVM options are used: -XX:+OptimizeStringConcat or -XX:+AggressiveOpts. More information here: Java7 Hotspot Loop Bug Details.

Update (01-Mar-2012): Bug was fixed in Java 6 update 29 and Java 7 update 1.

No comments:

Post a Comment