Core Java

If BigDecimal is the answer, it must have been a strange question

Overview

Many developers have determined that BigDecimal is the only way to deal with money.  Often they site that by replacing double with BigDecimal, they fixed a bug or ten.  What I find unconvincing about this is that perhaps they could have fixed the bug in the handling of double and that the extra overhead of using BigDecimal.

My comparison, when asked to improve the performance of a financial application, I know at some time we will be removing BigDecimal if it is there. (It is usually not the biggest source of delays, but as we fix the system it moves up to the worst offender).
 

BigDecimal is not an improvement

BigDecimal has many problems, so take your pick, but an ugly syntax is perhaps the worst sin.

  • BigDecimal syntax is an unnatural.
  • BigDecimal uses more memory
  • BigDecimal creates garbage
  • BigDecimal is much slower for most operations (there are exceptions)

The following JMH benchmark demonstrates two problems with BigDecimal, clarity and performance.

The core code takes an average of two values.

The double implementation looks like this. Note: the need to use rounding.

mp[i] = round6((ap[i] + bp[i]) / 2);

The same operation using BigDecimal is not only long, but there is lots of boiler plate code to navigate

mp2[i] = ap2[i].add(bp2[i])
     .divide(BigDecimal.valueOf(2), 6, BigDecimal.ROUND_HALF_UP);

Does this give you different results? double has 15 digits of accuracy and the numbers are far less than 15 digits. If these prices had 17 digits, this would work, but nor work the poor human who have to also comprehend the price (i.e. they will never get incredibly long).

Performance

If you have to incurr coding overhead, usually this is done for performance reasons, but this doesn’t make sense here.

BenchmarkModeSamplesScoreScore errorUnits
o.s.MyBenchmark.bigDecimalMidPricethrpt2023638.568590.094ops/s
o.s.MyBenchmark.doubleMidPricethrpt20123208.0832109.738ops/s

Conclusion

If you don’t know how to use round in double, or your project mandates BigDecimal, then use BigDecimal. But if you have choice, don’t just assume that BigDecimal is the right way to go.

The code

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.math.BigDecimal;
import java.util.Random;

@State(Scope.Thread)
public class MyBenchmark {
    static final int SIZE = 1024;
    final double[] ap = new double[SIZE];
    final double[] bp = new double[SIZE];
    final double[] mp = new double[SIZE];

    final BigDecimal[] ap2 = new BigDecimal[SIZE];
    final BigDecimal[] bp2 = new BigDecimal[SIZE];
    final BigDecimal[] mp2 = new BigDecimal[SIZE];

    public MyBenchmark() {
        Random rand = new Random(1);
        for (int i = 0; i < SIZE; i++) {
            int x = rand.nextInt(200000), y = rand.nextInt(10000);
            ap2[i] = BigDecimal.valueOf(ap[i] = x / 1e5);
            bp2[i] = BigDecimal.valueOf(bp[i] = (x + y) / 1e5);
        }
        doubleMidPrice();
        bigDecimalMidPrice();
        for (int i = 0; i < SIZE; i++) {
            if (mp[i] != mp2[i].doubleValue())
                throw new AssertionError(mp[i] + " " + mp2[i]);
        }
    }

    @Benchmark
    public void doubleMidPrice() {
        for (int i = 0; i < SIZE; i++)
            mp[i] = round6((ap[i] + bp[i]) / 2);
    }

    static double round6(double x) {
        final double factor = 1e6;
        return (long) (x * factor + 0.5) / factor;
    }

    @Benchmark
    public void bigDecimalMidPrice() {
        for (int i = 0; i < SIZE; i++)
            mp2[i] = ap2[i].add(bp2[i])
            .divide(BigDecimal.valueOf(2), 6, BigDecimal.ROUND_HALF_UP);
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(".*" + MyBenchmark.class.getSimpleName() + ".*")
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
JEt
JEt
9 years ago

When you deal with financial systems the performance is a good thing but accuracy more so.

Ask any stakeholder what would they like you to do, nobody will have the guts to say use double as its faster.

Hence, while they don’t solve bugs they make sure to protect themselves.

Shai Almog
9 years ago

I don’t know about your country but here we actually had a lawsuite worth close to a billion over rounding errors in banks. Double is designed for scientific data and is inherently inaccurate for some specific calculations, over time due to compound interest this can become a major problem. These things are very hard to notice and simulate in the short term, but I ran into quite a few banking systems where the code reviewers forced a rewrite in the other direction (devs used doubles and the banks demanded transition to BigDecimal). I’m not a fan of BigDecimal but unfortunately… Read more »

Fahd Shariff
9 years ago

It looks like you are going against Joshua Bloch’s recommendation in Effective Java Item 48: ‘Avoid float and double if exact answers are required’. It states that ‘the float and double types are particularly ill-suited for monetary calculations’ and his examples show that rounding doesn’t always work. I’d be interested to see you take his examples and make them work using doubles.

Germann Arlington
Germann Arlington
9 years ago

What is wrong with using native long in any language to represent money in lowest denomination (i.e. pence, cents…)?
It should give you enough precision too and rounding is handled very easily too.

Yannick Majoros
Yannick Majoros
9 years ago

It will end up doing what BigDecimal does, but you’ll have to handle the scaling yourself. Even if easy, doing it everywhere will eventually lead to bugs.

Alejandro D.
Alejandro D.
9 years ago

Because native floating point has inherent representation errors for decimal numbers in any language.
See http://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency and http://c2.com/cgi/wiki?FloatingPointCurrency.
And yes, this happens in real life. I saw it happen in languages so varied as COBOL, C++ and Java, from DOS to Linux.
I’m sorry but the author of this article has no idea of what he is talking about. I wouldn’t do what he suggest for a 10x increase in performance if we are talking about money.

Alejandro D.
Alejandro D.
9 years ago

Sorry, didn’t saw that you asked for the use of long. A big native integer type doesn’t have the representation errors that floating point has but you have to be extra careful with rounding, scaling, formatting/parsing and specially overflow. You have to do all that manually because the language doesn’t has any support for these tasks. For example if one variable has 2 decimal places and another 4 you must remember it every time to convert them manually for calculations. It soon becomes verbose and error prone. Long may seem very big if you always use only 2 decimal places… Read more »

Back to top button