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
.
- Test
BigDecimal
computations (execution average time: ~ 767 ms)
We got around 0.003 ms per computation. Not bad, but wait to see how fast primitiveBigDecimal 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)); } }
double
is. Next test will show us how much time we spend on creation ofBigDecimal
objects. - Test
BigDecimal
constructions (execution average time: ~ 468 ms)
As you can see, only construction of initial operands (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); } }
x
andy
) took around 61% of computation time. We can assume that rest 39% are also spent onBigDecimal
object creations (results ofpow()
andadd()
operations). Knowing thatBigDecimal
objects are immutable, let’s see how much we can gain by cachingBigDecimal
instances. - Test
BigDecimal
computations with cache (execution average time: ~ 309 ms)
Cache was prepared as follows: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)); } }
As you can see, cachingMap<Double, BigDecimal> cache = new HashMap<Double, BigDecimal>(); for (double x = 0.5; x <= 500; x += 0.5) { cache.put(x, BigDecimal.valueOf(x)); }
BigDecimal
gives a performance boost. Now it’s time to see how the primitivedouble
type is performing. - Test
double
computations (execution average time: ~ 2 ms)
Computation speed is great but unfortunately floating point numbers cannot be used for monetary calculations.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; } }
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:
Test | Average Exec Time | Performance Boost | |
Normal | -XX:+AggressiveOpts | ||
BigDecimal computations | 767 ms | 644 ms | 16% |
BigDecimal constructions | 468 ms | 394 ms | 16% |
BigDecimal computations with cache | 309 ms | 309 ms | 0% |
double computations | 2 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 isBigDecimal(String)
, or in case you want to convert an existing double into aBigDecimal
you can useBigDecimal.valueOf(double)
static method. - When dividing always specify a
RoundingMode
. Instead ofdivide(BigDecimal)
usedivide(BigDecimal, RoundingMode)
. This has to be done in order to avoidArithmeticException
. Below is an excerpt fromBigDecimal
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