已掉线,重新登录

首页 > 绿虎论坛 > 历史版块 > 编程 > PHP > 讨论/求助

标题: 一个想不通的关于精度问题的BUG

作者: @Ta

时间: 2019-05-16

点击: 9496


9c951e88a5d756c0789bbbe16d96c09f146525.jpg

[隐藏样式|查看源码]


『回复列表(17|隐藏机器人聊天)』

1. 为什么这个 BUG的点是0.06呢,其他的小数点没问题的
(/@Ta/2019-05-16 22:59//)

2.

你先打印$actual_money,看看精度有没有丢失。如果已经是丢失了,就用高精度的数学运算函数来做运算,而不是简单的A + B
小米5s Plus

(/@Ta/2019-05-16 23:13//)

3. @读书顶个鸟用,打印就是0.06
(/@Ta/2019-05-17 00:08//)

4. 为什么不用整形存储金额呢
(/@Ta/2019-05-20 15:01//)

5.

0.01无法用浮点数精确表示,每次累加结果都比你预期的多一点或者少一点,直到6次之后超出了你PHP字符串转换的精度阙值表现了出来。
看这个例子:

<?php
for ($d=0.0; $d < 0.1; $d +=0.01) {
        var_dump(sprintf('%0.32f',$d));
}

string(34) "0.00000000000000000000000000000000"
string(34) "0.01000000000000000020816681711722"
string(34) "0.02000000000000000041633363423443"
string(34) "0.02999999999999999888977697537484"
string(34) "0.04000000000000000083266726846887"
string(34) "0.05000000000000000277555756156289"
string(34) "0.06000000000000000471844785465692"
string(34) "0.07000000000000000666133814775094"
string(34) "0.08000000000000000166533453693773"
string(34) "0.08999999999999999666933092612453"
string(34) "0.09999999999999999167332731531133"

@胡椒舰长,不建议使用浮点数表示需要精确表示的数据类型。可以考虑使用整数表示法(*100)或者定点数函数库。
如果一定要使用浮点数,应该使用sprintf('%0.2f', $d)转换为字符串。PHP的(string)操作默认精度应该是sprintf('%0.14f', $d),不同版本的PHP可能不同。

你的代码大部分情况下没出问题,只是因为误差大小没有达到默认精度阙值。一但误差大小达到阙值,浮点数的默认字符串转换结果就会变成例子中的那样(0.0600000000000000047等),导致与数据库中的定点数不匹配。如果不自己控制精度(比如sprintf('%0.2f',$d)),出问题是迟早的事。

(/@Ta/2019-05-21 12:38//)

6.

浮点数累加后精度误差会扩大。以下例子展示了需要多久后才能产生默认设置中可见的差异:

<?php
for ($d=0.0; true; $d +=0.01) {
        $a = (string)$d;
        $b = sprintf('%0.2f', $d);
        if ($a != $b) {
                var_dump($a, $b);
                break;
        }
}

在我的PHP中,结果是:

string(15) "4.4299999999999"
string(4) "4.43"

结果可能会因为PHP版本的不同而不同。

(/@Ta/2019-05-21 13:07//)

7.

此外,楼主的代码还有一个偶然因素,$param['money']是多少并不确定。
所以,浮点数不仅可以无法精确表示0.01,也可以恰好无法精确表示$param['money'],并且误差较大。此时,两个数加起来的误差完全可以直接超过PHP字符串转换精度阙值。

(/@Ta/2019-05-21 12:51//)

8.

对于定点模拟(用整数来表示定点数),要记得最后格式化的时候不能转换为浮点数(也就是*100并强制转换为整数后不能/100变回去),而是应该使用字符串处理函数添加小数点。否则在变成浮点数后,再转换成整数时依然可以产生误差。
或者,最好的方法是从接收参数开始就转换成整数(用字符串处理函数去掉小数点),全程不要有小数,最后运算完了再通过字符串处理函数添加小数点,这样就不会产生误差。

(/@Ta/2019-05-21 13:12//)

9. @老虎会游泳,确实,没有考虑这么细致。
如果用户输入的是 0.1而非0.10,那应该是(int)0.1*100,然后
转成String?
(/@Ta/2019-05-21 14:38//)

10.

@胡椒舰长,考虑用字符串处理函数
$d = explode('.', $d);
$d = (int)($d[0].str_pad(substr($d[1], 0, 2), 2, '0'));

(/@Ta/2019-05-22 12:40//)

11.

最近项目中后端同学使用的是node.js也出现了精度问题导致验证支付失败,在掘金上刷文章又看到精度问题的文章,看QQ群的thinkphp周刊里又提到运算精度问题,逛葫芦林也看到类似文章 ...快入魔了

这是什么原因?@老虎会游泳,那么如果是电商场景,有一些优惠券之类的加减运算操作小数的,代码层以及数据库的最佳实践是什么?
小米5黑色低配版

(/@Ta/2019-05-27 13:40//)

12.

备注:下文中 ^ 表示乘方运算,不是异或运算。

@水木易安,对于金融相关计算,以下是建议:

  1. 如果有选择,不要使用任何浮点类型,double、float、只要是小数位数不定长的都不建议使用,因为通常都会产生难以发现的精度问题。如果没有选择(比如lua只支持double型),就不要在计算中产生小数(如果产生了,要立即舍入到整数范围)。
  2. 在数据库中使用定点数存储金额(比如mysql的decimal(14, 2))。也可以使用整数存储金额,把单位设置为分。
  3. 在计算中,尽量使用定点数。如果程序语言不支持定点数,可以考虑扩大100倍然后使用整数运算,运算完成后转换回定点数。注意,从数据库中拿到定点数后不应该直接*100,这可能会导致数据从字符串转换为浮点数然后*100,导致精度问题。建议使用sql语句*100,或者拿到结果后用字符串操作进行转换(直接去掉小数点)。转换回定点数的时候也一样,添加小数点。
  4. 如果你使用整数分存储金额,那只要在计算和存储过程中一直使用整数就没问题啦,轻松愉快。所以可能这才是最佳实践。至于显示,让前端添加小数点就可以(甚至可以考虑css添加小数点)。
  5. IEEE754双精度浮点型的基本表示法如下:
// double的二进制表示: <1bit符号位><11bit指数><52bit尾数>
$double的值 = bindec("1$尾数") * pow(2, $指数-1023) * ($符号位==1 ? -1 : 1);

尾数有一个隐含位1,也就是说它总是1开头,所以开头的1不包含在二进制表示里面。在浮点计算完成后,结果的尾数部分必须表示为1开头,也就是把尾数的前导0去掉,直到开头为1,同时缩小指数部分,然后删掉开头的1,这一过程叫做浮点数的“规格化”。

并不是所有浮点数都是“规格化”的,某些浮点数值是非规格化的,比如0(因为具有隐含位,底数的最小值为1,1的任意次方都不等于0,所以0没有办法规格化,必须用特殊方法表示)。

具体来说,浮点数的表示方法分为以下几种情况:

  1. 可以使用 y = 0b1<x> * 2^n 形式精确表示的数,没有精度损失。
  2. 完全不能使用 y = 0b1<x> * 2^n 形式表示,所以用了特殊方法表示的数,没有精度损失。
  3. 可以使用 y = 0b1<x> * 2^n 形式表示,但尾数的位数不够多,或者尾数出现“无限循环”的情况,此时出现精度损失。

所以,以下行为是精度安全的:

  • 在浮点型中存储整数。只要整数的范围是在-2^($尾数位数+1)-12^($尾数位数+1)-1内就可以(+1是因为隐含位)。在这种情况下,整数的最高位1被去掉,剩下的部分填充尾数(高位对齐),然后指数的值是整数的二进制位数。(0则是非规格化的,也是精确的。)
  • 两个浮点型表示的整数相加减。因为结果依然是整数,只要结果不超出上述范围就可以。
  • 两个浮点型表示的整数相乘。结果依然是整数,只要不超出上述范围就可以。

以下行为不是精度安全的:

  • 浮点数值包含小数部分。很多常见的两位小数都不能用浮点型精确表示,结果的尾数部分是无限循环。
  • 两个浮点型整数相除。因为结果可能包含小数部分,导致不精确。
(/@Ta/2019-05-27 15:46//)

13.

上面说到最好只使用整数运算,那么怎么用整数计算打折呢?
比如75折:
y = x * 75 / 100
只要乘法不溢出,结果就是良好的。在大部分编程语言中结果都还是整数。如果不是整数,舍入到整数即可。

(/@Ta/2019-05-27 15:50//)

14.

@水木易安,哦对了,其实如果只是为了防止“相等”比较时出现不匹配的问题,只需要做以下两件事就可以了:

  1. 数据库中使用decimal(n, 2)
  2. 比较前对数据使用round($x, 2)并且转换为字符串再发送给数据库。或者sprintf('%0.2f', $x)
(/@Ta/2019-05-27 15:55//)

15. @老虎会游泳,似乎老虎没有注意到 并不存在“阙值”这个词。 
(/@Ta/2019-05-27 17:18//)

16.

@老虎会游泳,学习了
小米5黑色低配版

(/@Ta/2019-05-28 11:52//)

17.

@iola1999,阙值和阈值长的那么像,你居然看得出来

(/@Ta/2019-05-29 20:47//)

回复需要登录

8月26日 12:16 星期二

本站由hu60wap6驱动

备案号: 京ICP备18041936号-1