在处理货币计算时,我可以看到“双倍精度”存在四种基本的坑人方法。
尾数太小
尾数只有 ~15 个十进制数字的精度,在处理比这更大金额时会得到错误结果。如果您要跟踪美分,则在 1013(一万亿)美元之前就会出现问题。
虽然这是一个很大的数字,但也不是“那么大”。美国的 18 万亿美元的 GDP 超过了它,因此在处理国家甚至公司规模的金额时可能会轻易出现错误答案。
此外,在计算过程中,有很多小额金额可能会超过此阈值。您可能正在进行增长预测或多年的分析,从而得出一个大的最终价值。您可能正在进行“假设”情景分析,其中会检查各种可能的参数,某些参数的组合可能会导致非常大的值。您可能正在遵守允许使用
美分的金融规则,这可能会将您的范围再减少两个数量级或更多,使您与美元中
普通人的财富大致相当。
最后,让我们不要以美国为中心看待事物。其他货币呢?一美元大约相当于13,000印尼盾,因此在该货币中,您需要跟踪货币金额的另外两个数量级(假设没有“分”!)。您几乎可以接近普通人感兴趣的金额了。
这里是一个例子,从10亿开始以5%的增长率进行的增长预测计算出现了错误:
method year amount delta
double 0 $ 1,000,000,000.00
Decimal 0 $ 1,000,000,000.00 (0.0000000000)
double 10 $ 1,628,894,626.78
Decimal 10 $ 1,628,894,626.78 (0.0000004768)
double 20 $ 2,653,297,705.14
Decimal 20 $ 2,653,297,705.14 (0.0000023842)
double 30 $ 4,321,942,375.15
Decimal 30 $ 4,321,942,375.15 (0.0000057220)
double 40 $ 7,039,988,712.12
Decimal 40 $ 7,039,988,712.12 (0.0000123978)
double 50 $ 11,467,399,785.75
Decimal 50 $ 11,467,399,785.75 (0.0000247955)
double 60 $ 18,679,185,894.12
Decimal 60 $ 18,679,185,894.12 (0.0000534058)
double 70 $ 30,426,425,535.51
Decimal 70 $ 30,426,425,535.51 (0.0000915527)
double 80 $ 49,561,441,066.84
Decimal 80 $ 49,561,441,066.84 (0.0001678467)
double 90 $ 80,730,365,049.13
Decimal 90 $ 80,730,365,049.13 (0.0003051758)
double 100 $ 131,501,257,846.30
Decimal 100 $ 131,501,257,846.30 (0.0005645752)
double 110 $ 214,201,692,320.32
Decimal 110 $ 214,201,692,320.32 (0.0010375977)
double 120 $ 348,911,985,667.20
Decimal 120 $ 348,911,985,667.20 (0.0017700195)
double 130 $ 568,340,858,671.56
Decimal 130 $ 568,340,858,671.55 (0.0030517578)
double 140 $ 925,767,370,868.17
Decimal 140 $ 925,767,370,868.17 (0.0053710938)
double 150 $ 1,507,977,496,053.05
Decimal 150 $ 1,507,977,496,053.04 (0.0097656250)
double 160 $ 2,456,336,440,622.11
Decimal 160 $ 2,456,336,440,622.10 (0.0166015625)
double 170 $ 4,001,113,229,686.99
Decimal 170 $ 4,001,113,229,686.96 (0.0288085938)
double 180 $ 6,517,391,840,965.27
Decimal 180 $ 6,517,391,840,965.22 (0.0498046875)
double 190 $ 10,616,144,550,351.47
Decimal 190 $ 10,616,144,550,351.38 (0.0859375000)
差值(double
和BigDecimal
之间的差异)在160年时首次超过1美分,约为2万亿(也许从现在起160年并不算太多),当然这个差距只会越来越大。
当然,53位尾数意味着这种计算的相对误差可能非常小(希望你不会因为2万亿中的1美分而失去工作)。实际上,相对误差在大部分示例中基本保持稳定。您当然可以将其组织起来,例如通过减去两个具有尾数精度损失的数字,从而导致任意大的误差(读者可自行练习)。
更改语义
所以你认为自己很聪明,想出了一个舍入方案,可以使用double
并已在本地JVM上进行了详尽的测试。那就部署吧。明天、下周或者最糟糕的时间,结果会发生变化,你的技巧就会失效。
与几乎所有其他基本语言表达式以及整数或BigDecimal
算术不同,由于strictfp特性,许多浮点表达式的结果默认情况下没有单一的标准定义值。平台可以自行决定使用更高精度的中间值,在不同的硬件、JVM版本等上可能会导致不同的结果。对于相同的输入,当方法从解释执行切换到JIT编译时,结果甚至可能在运行时发生变化!
如果您在Java 1.2之前编写代码,当Java 1.2突然引入现在默认的变量FP行为时,您可能会感到非常恼火。您可能会尝试在所有地方都使用strictfp
,并希望您不会遇到任何相关错误的大量问题 - 但在某些平台上,您将放弃双倍带给您的大部分性能。
可以说,JVM规范未来可能会再次更改以适应FP硬件的进一步变化,或者JVM实现者可能会利用默认的非严格FP行为给他们带来的优势来做一些巧妙的事情。
不精确的表示
正如Roland在他的回答中指出的那样,double
的一个关键问题是它没有某些非整数值的精确表示。虽然单个非精确值(例如0.1
)在某些情况下通常可以“往返”(例如Double.toString(0.1).equals("0.1")
),但只要对这些不精确的值进行数学运算,误差就会累积,这可能是无法恢复的。
特别是,如果您“接近”一个舍入点,例如~1.005,当真实值为1.0050000001...时,您可能会得到1.00499999...的值,
反之亦然。由于误差方向相反,因此没有舍入魔法可以解决这个问题。无法确定1.004999999...的值是否应该上调。您的
roundToTwoPlaces()
方法(一种双重舍入)之所以有效,是因为它处理了1.0049999应该上调的情况,但它永远无法跨越边界,例如,如果累积误差导致1.0050000000001变成1.00499999999999,则无法修复它。
您不需要大或小的数字来达到这个目的。您只需要进行一些数学计算,并使结果接近边界即可。您进行的数学计算越多,可能偏离真实结果的范围就越大,同时也更有可能跨越边界。
根据请求,这里有一个搜索测试,它进行了简单的计算:amount * tax
并将其四舍五入到2个小数位(即美元和美分)。其中有几种舍入方法,当前使用的roundToTwoPlacesB
是您的升级版1(通过增加第一轮舍入中n
的乘数,使其更加敏感——原始版本在微不足道的输入上立即失败)。
测试会输出它找到的失败,并且它们会成批出现。例如,前几次失败:
Failed for 1234.57 * 0.5000 = 617.28 vs 617.29
Raw result : 617.2850000000000000000000, Double.toString(): 617.29
Failed for 1234.61 * 0.5000 = 617.30 vs 617.31
Raw result : 617.3050000000000000000000, Double.toString(): 617.31
Failed for 1234.65 * 0.5000 = 617.32 vs 617.33
Raw result : 617.3250000000000000000000, Double.toString(): 617.33
Failed for 1234.69 * 0.5000 = 617.34 vs 617.35
Raw result : 617.3450000000000000000000, Double.toString(): 617.35
请注意,“原始结果”(即精确的未舍入结果)总是接近边界。您的舍入方法会在高和低两侧出错。你不能通用地修复它。
不精确计算
java.lang.Math
的几个方法并不要求正确舍入的结果,而是允许误差达到2.5 ulp。当然,你可能不会经常在货币计算中使用双曲函数,但是诸如exp()
和pow()
等函数经常在货币计算中出现,而这些函数只有1 ulp的精度。所以当返回这个数字时,它已经是“错误”的。
这与“不精确表示”问题相互作用,因为这种类型的错误比普通数学运算引起的错误更严重,后者至少从可表示的域中选择最佳值。这意味着当您使用这些方法时,可能会有更多的舍入边界穿越事件发生。
double
)。 - BeeOnRope