背景与常见场景

  • 高精度的数值计算也常用在货币处理上。现在业务上通常默认使用BigDecimal来处理货币相关数据。但是高精度的使用还是有一些注意事项。本文对一些坑进行探讨。
  • BigDecimal性能上比Float差很多,误用情况下(如作为中间件,序列化反序列化等)会导致性能问题甚至DoS

Float和Double的精度丢失

由于对float或double 的使用不当,可能会出现精度丢失的问题。问题大概情况可以通过如下代码理解:

  1. public class FloatDoubleTest {
  2. public static void main(String[] args) {
  3. float f = 20014999;
  4. double d = f;
  5. double d2 = 20014999;
  6. System.out.println("f=" + f);
  7. System.out.println("d=" + d);
  8. System.out.println("d2=" + d2);
  9. }
  10. }

得到的结果如下:
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问题。

  1. public static int REPEAT_TIMES = 1000000;
  2. public static double computeByBigDecimal(double a, double b) {
  3. BigDecimal result = BigDecimal.valueOf(0);
  4. BigDecimal decimalA = BigDecimal.valueOf(a);
  5. BigDecimal decimalB = BigDecimal.valueOf(b);
  6. for (int i = 0; i < REPEAT_TIMES; i++) {
  7. result = result.add(decimalA.multiply(decimalB));
  8. }
  9. return result.doubleValue();
  10. }
  11. public static double computeByDouble(double a, double b) {
  12. double result = 0;
  13. for (int i = 0; i < REPEAT_TIMES; i++) {
  14. result += a * b;
  15. }
  16. return result;
  17. }
  18. public static void main(String[] args) {
  19. long test = System.nanoTime();
  20. long start1 = System.nanoTime();
  21. double result1 = computeByBigDecimal(0.120000000034, 11.22);
  22. long end1 = System.nanoTime();
  23. long start2 = System.nanoTime();
  24. double result2 = computeByDouble(0.120000000034, 11.22);
  25. long end2 = System.nanoTime();
  26. long timeUsed1 = (end1 - start1);
  27. long timeUsed2 = (end2 - start2);
  28. System.out.println("result by BigDecimal:" + result1);
  29. System.out.println("time used:" + timeUsed1);
  30. System.out.println("result by Double:" + result2);
  31. System.out.println("time used:" + timeUsed2);
  32. System.out.println("timeUsed1/timeUsed2=" + timeUsed1 / timeUsed2);
  33. }

运行结果如下:

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分钟!

  1. public static void main(String[] args) {
  2. long start2 = System.nanoTime();
  3. BigDecimal a = new BigDecimal("5");
  4. // BigDecimal b = new BigDecimal("5e912345");
  5. BigDecimal b = new BigDecimal("5");
  6. BigDecimal c = a.add(b);
  7. System.out.println(c);
  8. long end2 = System.nanoTime();
  9. long timeUsed2 = (end2 - start2);
  10. System.out.println("time used:" + timeUsed2);
  11. //time used:729889300
  12. // time used:1688200
  13. }

其他问题

曾经据说Java调用Double.parseDouble(“2.2250738585072012e-308”)可能会引起无限循环而导致DoS,但是我试验时无法复现(2011年的老漏洞了,可能已修复了,描述见【3】)

参考资料