Is there a way to make BigDecimal faster than I have here?

拥有回忆 提交于 2019-12-12 04:23:48

问题


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")
}

回答1:


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



回答2:


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



回答3:


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.



来源:https://stackoverflow.com/questions/39921191/is-there-a-way-to-make-bigdecimal-faster-than-i-have-here

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!