问题
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 typelong
, 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 Int
s:
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