精度损失而引发的 bug
Posted 机智的小小帅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精度损失而引发的 bug相关的知识,希望对你有一定的参考价值。
精度损失而引发的 bug
本周碰到因为 精度损失,导致 分段计算的结果之和 ≠ 整体计算的结果
基本背景
有一个佣金功能,需要计算销售人员每个月的佣金以及销售人员所有月份的总佣金。
佣金金额 = 销售额 * 佣金比例
其中 销售额 和 佣金 的精度都是当前货币的最小单位。目前货币单位为美元,最小单元为美分。
最初的实现 (有 bug)
单月佣金 = 单月销售额 * 佣金比例
总佣金 = 总计销售额 * 佣金比例
异常 case
上面的实现在一般情况下的确是没有问题的。
但是偶尔会出现: 总佣金 ≠ 单月佣金之和的情况
举个例子,假设佣金比例为 0.3
销售人员在一月的销售金额为 3333 美分,则他一月的佣金为 3333 * 0.3 = 999.9 -> 999 美分 (美分即为最小单位了, 直接取整)
销售人员在二月的销售金额为 3333 美分,则他二月的佣金为 3333 * 0.3 = 999.9 -> 999 美分
销售人员总计销售额为 6666 美分,则它总佣金为 6666 * 0.3 = 1999.8 -> 1999 美分
此时 999 + 999 = 1998 ≠ 1999
原因是将浮点数强行转换为整数而造成了精度损失。
这种精度损失是无法避免的,我们能做的只是让结果看起来不那么怪。
在这种情况下:用户发现 每个月的佣金之和 ≠ 总佣金,可能就会觉得非常奇怪。
如何让 每个月的佣金之和 = 总佣金 呢?
有两种解决方案
资本家的做法: 优化总佣金的计算方法
原来的佣金计算方法为
单月佣金 = 单月销售额 * 佣金比例
总佣金 = 总计销售额 * 佣金比例
现改为
单月佣金 = 单月销售额 * 佣金比例
总佣金 = 单月佣金之和
应用到上面的 case 里,总佣金就不在是 1999 了,而是 999 + 999 = 1998。
足足少支付了一分钱,资本家心满意足的离开现场
不贪小便宜的做法: 优化单月佣金的计算方法
总佣金 = 总计销售额 * 佣金比例
第 n 月佣金 = 第 (1 ~ n) 个月的总佣金 - 第 (1 ~ n - 1) 个月的总佣金
应用到上面的例子中:
一月的佣金 = 3333 * 0.3 - 0 = 999
二月的佣金 = (3333 + 3333) * 0.3 - 3333 * 0.3 = 1999 - 999 = 1000
总佣金 = 6666 * 0.3 = 1999 = 999 + 1000
没有贪墨劳动人民的一分血汗钱!
总结
由于进度损失
m * p + n * p ≠ (m + n) * p
的现象是无法避免的
可以使用加减法来替代乘法
因为
m * p + n * p = m * p + n * p
m * p + ((m + n) * p - (m * p)) = (m + n) * p
这两个等式是始终成立的
PHP 精度计算引发的灾难性Bug
在维护一个比较老的金融项目时,发现精度的计算简单粗暴,先来看一下代码片段:
if($accountInfo[\'account_money\'] < $repayMoney ){ rollback(); return false; }
如上代码片段变量所示,上面的代码主要是比较帐户余额及还款数,两者均为双精度浮点数,稍作修改代码,让我们能看到一些输出才更直观
if($accountInfo[\'account_money\'] < $repayMoney ){ echo "{$accountInfo[\'account_money\']}\\n\\r"; echo "{$repayMoney}\\n\\r"; rollback(); return false; }
运行如上代码得到如下图结果
两个数均为1333.35,如果按上面的逻辑来说,不应该进入到 if 体内,但结果却恰恰相反,进行 if 体内并echo 出了两值,通过查阅相关api 发现,PHP 的精度运算,会在直接+,-,*,/,<,>等运算时丢失精度,因此结果并非如你所愿,在这里提现大家,在PHP中对精度运算,应调用相关API,如BC Math.
以上是关于精度损失而引发的 bug的主要内容,如果未能解决你的问题,请参考以下文章