背景与常见场景
- 高精度的数值计算也常用在货币处理上。现在业务上通常默认使用BigDecimal来处理货币相关数据。但是高精度的使用还是有一些注意事项。本文对一些坑进行探讨。
- BigDecimal性能上比Float差很多,误用情况下(如作为中间件,序列化反序列化等)会导致性能问题甚至DoS
Float和Double的精度丢失
由于对float或double 的使用不当,可能会出现精度丢失的问题。问题大概情况可以通过如下代码理解:
public class FloatDoubleTest {
public static void main(String[] args) {
float f = 20014999;
double d = f;
double d2 = 20014999;
System.out.println("f=" + f);
System.out.println("d=" + d);
System.out.println("d2=" + d2);
}
}
得到的结果如下:
f=2.0015E7
d=2.0015E7
d2=2.0014999E7
从输出结果可以看出double 可以正确的表示20014999 ,而float 没有办法表示20014999 ,得到的只是一个近似值。
原理详情参考:http://singleant.iteye.com/blog/713890
BigDecimal的性能问题001
为了精度问题,我们常常不直接使用浮点数,常常是采用BigDecimal来替换浮点数。本文主要想探索一下BigDecimal相比于double性能上可能存在的一些问题。
BigDecimal的运算时间效率是double的很多倍。即若做在中间件,可能有DDoS问题。
public static int REPEAT_TIMES = 1000000;
public static double computeByBigDecimal(double a, double b) {
BigDecimal result = BigDecimal.valueOf(0);
BigDecimal decimalA = BigDecimal.valueOf(a);
BigDecimal decimalB = BigDecimal.valueOf(b);
for (int i = 0; i < REPEAT_TIMES; i++) {
result = result.add(decimalA.multiply(decimalB));
}
return result.doubleValue();
}
public static double computeByDouble(double a, double b) {
double result = 0;
for (int i = 0; i < REPEAT_TIMES; i++) {
result += a * b;
}
return result;
}
public static void main(String[] args) {
long test = System.nanoTime();
long start1 = System.nanoTime();
double result1 = computeByBigDecimal(0.120000000034, 11.22);
long end1 = System.nanoTime();
long start2 = System.nanoTime();
double result2 = computeByDouble(0.120000000034, 11.22);
long end2 = System.nanoTime();
long timeUsed1 = (end1 - start1);
long timeUsed2 = (end2 - start2);
System.out.println("result by BigDecimal:" + result1);
System.out.println("time used:" + timeUsed1);
System.out.println("result by Double:" + result2);
System.out.println("time used:" + timeUsed2);
System.out.println("timeUsed1/timeUsed2=" + timeUsed1 / timeUsed2);
}
运行结果如下:
result by BigDecimal:1346400.00038148
time used:365847335
result by Double:1346400.000387465
time used:5361855
timeUsed1/timeUsed2=68
从结果上来看BigDecimal给我们带来了精度上的提升,但是性能上的损耗是巨大的。同样的运算时间居然是double的68倍。
BigDecimal的性能问题002
如果有一个输入可以允许用户输入任意大的数据,短字符串“5e912345”代表一个巨大的数字,对大数字的操作往往比小数字上的操作慢。如果您希望算法在很宽的范围内具有稳定的性能,请使用内置的float或double浮点类型。如果需要考虑Web应用程序的行为,请根据您选择的条件验证输入。
譬如下述代码在intel双核处理器上运行该示例据说需要4分钟!
public static void main(String[] args) {
long start2 = System.nanoTime();
BigDecimal a = new BigDecimal("5");
// BigDecimal b = new BigDecimal("5e912345");
BigDecimal b = new BigDecimal("5");
BigDecimal c = a.add(b);
System.out.println(c);
long end2 = System.nanoTime();
long timeUsed2 = (end2 - start2);
System.out.println("time used:" + timeUsed2);
//time used:729889300
// time used:1688200
}
其他问题
曾经据说Java调用Double.parseDouble(“2.2250738585072012e-308”)可能会引起无限循环而导致DoS,但是我试验时无法复现(2011年的老漏洞了,可能已修复了,描述见【3】)