0

I'm working on some financial analysis software that will need to process large (ish) volumes of data. I'd like to use BigDecimal for the accuracy (some pricing info goes to four or five digits to the right of the decimal) but I was concerned about speed.

I wrote the following test app and it appears that BigDecimal can be 90 to 100 times slower than Doubles. I knew there would be a delta, but that's more than I was expecting. Here's a typical output after many trials.

BigDecimal took 17944 ms
Double took 181 ms

Am I missing something?

Here is the code. I tried to make it representative of real world. I created a constant where I could (pi) but also did some inline math of numbers that would vary from data row to data row - such as pi * BigDecimal(i) + BigDecimal(1). My point being that avoiding constructors can't be the only answer.

Fortunately, it appears Double has enough precision anyway since numbers will be typically in the format 00000.00000. Any hidden gotchas I should know about, though? Do people use Double for financial analysis software?

import java.math.BigDecimal

object Stopwatch {
    inline fun elapse(f: () -> Unit):Long {
        val start = System.currentTimeMillis()
        f()
        return System.currentTimeMillis() - start
    }
}


fun tryBigDecimal() {
    val arr: MutableList<BigDecimal> = arrayListOf()

    for (i in 1..10000000) {
        arr.add(BigDecimal(i))
    }

    val pi = BigDecimal(3.14159)

    for (i in 0..arr.size - 1) {
        arr[i] = arr[i] * pi / (pi * BigDecimal(i) + BigDecimal(1))
    }

    //arr.forEachIndexed { i, bigDecimal ->  println("$i, ${bigDecimal.toString()}")}
}

fun tryDouble() {
    val arr: MutableList<Double> = arrayListOf()

    for (i in 1..10000000) {
        arr.add(i.toDouble())
    }

    val pi = 3.14159

    for (i in 0..arr.size - 1) {
        arr[i] = arr[i] * pi / (pi * i + 1)
    }

    //arr.forEachIndexed { i, bigDecimal ->  println("$i, ${bigDecimal.toString()}")}

}

fun main(args: Array<String>) {
    val bigdecimalTime = Stopwatch.elapse(::tryBigDecimal)
    println("BigDecimal took $bigdecimalTime ms")

    val doubleTime = Stopwatch.elapse(::tryDouble)
    println("Double took $doubleTime ms")
}
mhasan
  • 3,703
  • 1
  • 18
  • 37
vocalionecho
  • 301
  • 4
  • 9
  • 3
    Don't use double when accuracy is important like with finances. It might have the precision needed but not the accuracy. Use int/long if you can, and BigDecimal if you can't, but never float/double. – puhlen Oct 07 '16 at 15:50
  • Since you took the time to provide constructive feedback, which I appreciate, I wanted to take the time to provide some to you, too. Your reply could have been written as "I see where you may have a problem. The optimizer may have eliminated a lot of the code in the Doubles section which would really invalidate the test. You may want to check out JMH which can help with just this kind of problem." The aggression and arrogance of your response may not have been intentional, but it was strong. Just saying... – vocalionecho Oct 07 '16 at 18:38
  • FWIW, what you are missing is that `double` is hardware supported on most systems, while `BigDecimal` is purely a software type. Not sure what role the optimizer plays, but you should expect a considerable difference in speed. – Rudy Velthuis Oct 07 '16 at 18:46
  • @vocalionecho thank you for responding. I apologise. Still, JMH is necessary to judge any alternatives for this issue. – voddan Oct 07 '16 at 22:39
  • I sincerely appreciate the help @voddan. You obviously know what you're talking about. I will definitely look into JMH. – vocalionecho Oct 08 '16 at 00:30

3 Answers3

3

You can try Moneta, the JSR 354 reference implementation (JavaMoney RI). It has a FastMoney implementation:

FastMoney represents numeric representation that was optimized for speed. It represents a monetary amount only as a integral number of type long, hereby using a number scale of 100'000 (10^5).

e.g.

operator fun MonetaryAmount.times(multiplicand: Double): MonetaryAmount {
    return multiply(multiplicand)
}

operator fun MonetaryAmount.div(divisor: Double): MonetaryAmount {
    return divide(divisor)
}

fun tryFastMoney() {
    val currency = Monetary.getCurrency("USD")
    val arr: MutableList<MonetaryAmount> = arrayListOf()

    for (i in 1..10000000) {
        arr.add(FastMoney.of(i, currency))
    }

    val pi = 3.14159

    for (i in 0..arr.size - 1) {
        arr[i] = arr[i] * pi / (pi * i + 1)
    }
}

fun main(args: Array<String>) {
    val fastMoneyTime = Stopwatch.elapse(::tryFastMoney)
    println("FastMoney took $fastMoneyTime ms")
    val doubleTime = Stopwatch.elapse(::tryDouble)
    println("Double took $doubleTime ms")
}
FastMoney took 7040 ms
Double took 4319 ms
mfulton26
  • 29,956
  • 6
  • 64
  • 88
2

Yes, BigDecimal is appropriate for money. Or any other situation where you need accuracy rather than speed.

Floating-point

The float, Float, double, and Double types all use floating-point technology.

The purpose of floating-point is to trade away accuracy for speed of execution. So you often see extraneous incorrect digits at the end of the decimal fraction. This is acceptable for gaming, 3D visualizations, and many scientific applications. Computers commonly have specialized hardware to accelerate floating point calculations. This is possible because the IEEE has concretely standardized floating point behavior.

Floating-point is not acceptable for financial transactions. Nor is floating point acceptable in any other situation that expects correct fractions.

BigDecimal

The two purposes of BigDecimal are:

  • Handle arbitrarily large/small number.
  • Not use floating point technology.

So, what does your app need? Slow but accurate? Or, fast but slightly inaccurate? Those are your choices. Computers are not magic, computers are not infinitely fast nor infinitely accurate. Programming is like engineering in that it is all about choosing between trade-offs according to the needs of your particular application.

BigDecimal is one of the biggest sleeper features in Java. Brilliant work by IBM and others. I don't know if any other development platform has such an excellent facility for accurately handling decimal numbers. See some JavaOne presentations from years ago if you want to appreciate the technical issues.

Do not initialize a BigDecimal object by passing a float or double:

new BigDecimal( 1234.4321 )  // BAD - Do not do this.

That argument creates a float value which introduces the inaccuracies of floating point technology. Use the other constructors.

new BigDecimal( "1234.4321" )  // Good
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
1

The most common solution for finances is using Int or several Ints:

val pi = 314159  // the point is implicit. To get the real value multiply `pi * 0.00001` 

That way you explicitly control everything about the numbers (e.i. the remainders after a division).

You may use Long, but it is not atomic, and thus it is not concurrently safe. Which means that you have to synchronise on any shared Long you have.

A rule of thumb is to never ever use Floating Point Arithmetics (e.i. Double or Float) for finances, because, well, its point floats, thus guaranteeing absolutely nothing when the numbers are big.

voddan
  • 31,956
  • 8
  • 77
  • 87