不使用 bcmath 系列函数,关于 PHP 中 float 乘法的精度问题 的再困惑

2017-10-05 22:35:41 +08:00
 xiaoyanbot

在不使用 bcmath 系列函数下,为何精准二进制打印出来是失真的,但最后计算结果却是对的, 为何?

(使用的 PHP 版本 5.4、5.6 和 7.1 得到的结果一致 )

以下运算,模拟 $a 为商品价格(带两位小数), $b 为数量(整数)。 困惑的是, 是否这一类的电商运算都采用 bcmath 系列函数来做? MySQL 后端存储为 decimal 类型,这个没疑惑。

先看看结果为:

代码为:

<?php

$a = 1.23;
$b = 100000;

printf("%0.30f", $a);
echo '<br />';
printf("%0.30f", $b);

echo "<hr>";
echo '$a 的数据类型:  ';
var_dump($a);echo "<hr>";
echo '$b 的数据类型:  ';
var_dump($b);echo "<hr>";

$total = $a * $b;

echo '计算结果:  ';
var_dump($total);echo "<hr>";
echo '高精度计算结果:  ';
printf("%0.30f", $total);
3180 次点击
所在节点    PHP
12 条回复
xiaoyanbot
2017-10-05 22:38:33 +08:00
难道 * 这个运算符, 自动作了四舍五入的处理了?
xiaoyanbot
2017-10-05 22:40:50 +08:00
又进一步测试,发现当 $b 为 10 的时候, 最终结果是失真的

结果为:
~~~
1.229999999999999982236431605997
10.000000000000000000000000000000
$a 的数据类型:float(1.23)
$b 的数据类型:int(10)
计算结果:float(12.3)
高精度计算结果:12.300000000000000710542735760100
~~~

当$b 为 100 的时候,最终结果不失真

结果为:
~~~
1.229999999999999982236431605997
100.000000000000000000000000000000
$a 的数据类型:float(1.23)
$b 的数据类型:int(100)
计算结果:float(123)
高精度计算结果:123.000000000000000000000000000000
~~~
WuwuGin
2017-10-06 00:40:25 +08:00
PHP 遵循 IEEE 754 双精度:
浮点数, 以 64 位的双精度, 采用 1 位符号位(E), 11 指数位(Q), 52 位尾数(M)表示(一共 64 位).
符号位:最高位表示数据的正负,0 表示正数,1 表示负数。
指数位:表示数据以 2 为底的幂,指数采用偏移码表示
尾数:表示数据小数点后的有效数字.
再来看看小数用二进制怎么表示:
乘 2 取整,顺序排列,即将小数部分乘以 2,然后取整数部分,剩下的小数部分继续乘以 2,然后取整数部分,剩下的小数部分又乘以 2,一直取到小数部分,但是像 0.57 这样的小数像这样一直乘下去,小数部分不可能为 0.有效位的小数用二进制表示却是无穷的。
0.57 的二进制表示基本上(52 位)是: 0010001111010111000010100011110101110000101000111101
如果只有 52 位的话,0.57 =》 0.56999999999999995
不难看出上面意外的结果了吧。
ref:http://www.jb51.net/article/65984.htm
Sikoay
2017-10-06 00:44:51 +08:00
难道不是都采用整数来进行运算和存储吗,float 永远无法准确地计算一个数(忘记是不是这样说的了,effective Java 中有写过
Sikoay
2017-10-06 00:49:35 +08:00
我们是将金额乘以 100 (精确到小数点两位后),然后运算和存储,显示的时候再将计算之后的值除以 100
eoo
2017-10-06 09:40:27 +08:00
楼上+1 腾讯也是这么做滴
xiaoyanbot
2017-10-06 10:36:45 +08:00
@eoo 能确认: 腾讯内部都是这么做的吗?

有没有人做过使用 bcmath 系列函数,和 采用 int 乘再除 这两种方式的压测效率之类的?
xiaoyanbot
2017-10-06 10:37:06 +08:00
阿里的 Java 手册,关于这一部分的描述
~~~
6. [强制] 小数类型为 decimal,禁止使用 float 和 double。
说明:float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不
正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
~~~
eoo
2017-10-06 11:06:23 +08:00
@xiaoyanbot 其他我不知道 我只知道 我抓 QQ 支付记录包的时候 返回的都是 支付金额 x 100 你想一下 QQ 支付系统都这样做了 你觉得其他的会有什么不一样?
Sikoay
2017-10-06 12:37:19 +08:00
@eoo 写过微信支付的都知道这一点吧
blackshadow
2017-10-06 16:39:26 +08:00
楼上说的,支付用金额 x100。是因为人民币最小单位是分,这样计算的金额,利息等比较准确。几乎所有的支付系统都是按分存取的吧。在我碰到的 PHP 支付系统里确实有使用了 bcmath 系统函数。至少我做过的是这样。
xiaoyanbot
2017-10-08 17:38:33 +08:00
@blackshadow

请问有测试过使用 bcmath 函数 和 使用 *100, 再除以 100 的运算效率对比吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/395568

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX