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();
    }
}
Related Whitepaper:

Bulletproof Java Code: A Practical Strategy for Developing Functional, Reliable, and Secure Java Code

Use Java? If you do, you know that Java software can be used to drive application logic of Web services or Web applications. Perhaps you use it for desktop applications? Or, embedded devices? Whatever your use of Java code, functional errors are the enemy!

To combat this enemy, your team might already perform functional testing. Even so, you're taking significant risks if you have not yet implemented a comprehensive team-wide quality management strategy. Such a strategy alleviates reliability, security, and performance problems to ensure that your code is free of functionality errors.Read this article to learn about this simple four-step strategy that is proven to make Java code more reliable, more secure, and easier to maintain.

Get it Now!  

7 Responses to "If BigDecimal is the answer, it must have been a strange question"

  1. JEt says:

    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.

  2. Shai Almog says:

    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 I don’t see an option, this is a pretty old concept the x86 chip has builtin support for BCD and that COBOL used it internally too.

  3. 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.

  4. Germann Arlington says:

    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 says:

      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. says:

      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. says:

      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 but if you need to use lots of decimal digits for a calculation and the numbers are also big on the integer side -happens in financial operations- you will soon hit the hard limits of the type.
      Also your code may well become very difficult to change.
      This technique is useful sometimes, keep it in your toolbox, but be very careful of when you apply it.

Leave a Reply


5 − = three



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.
Do you want to know how to develop your skillset and become a ...
Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you two of our best selling eBooks for FREE!

Get ready to Rock!
You can download the complementary eBooks using the links below:
Close