最近在改代码的时候发现在处理浮点型类型的数据时会出现精度丢失的问题,下面我们来看下例子
javascript:
0.1 + 0.2 为啥不等于 0.3 ? (正确结果:0.30000000000000004)
0.8 * 7 为啥不等于 5.6 ? (正确结果:5.6000000000000005)
PHP:
$a = 0.58;
$b = 100000;
$c = floor($a * $b);
var_dump($c);
exit;
输出:float(57999)
上面的结果,显然不是我们想要的!
1、浮点运算惹的祸
其实这些结果都并非语言的 bug,但和语言的实现原理有关, js 所有数字统一为 Number, 包括整型实际上全都是双精度(double)类型。
而PHP会区分 int 还是 float。不管什么语言,只要涉及浮点运算,都是存在类似的问题,使用时一定要注意。
PHP 官方手册解释如下:
浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数 或者 gmp 函数。
这里的关键在于,浮点数的小数用二进制的表示,过程如下:
- 将小数乘以2,取整数部分表示第一位;
- 将小数部分乘以2,取整数部分表示第二位;
- 再将小数部分乘以2,取整数部分表示第三位;
- … 依次类推,直到小数部分为0;
例:0.58
- 0.58 * 2 = 1.16 —> 1
- 0.16 * 2 = 0.32 —> 0
- 0.32 * 2 = 0.64 —> 0
- 0.64 * 2 = 1.28 —> 1
- 0.28 * 2 = 0.56 —> 0
- 0.56 * 2 = 1.12 —> 1
- 0.12 * 2 = 0.24 —> 0
- 0.24 * 2 = 0.48 —> 0
- 0.48 * 2 = 0.96 —> 0
- 0.96 * 2 = 1.92 —> 1
- …
我们会得到一个无限循环的二进制小数:
0.1001010001…
小数部分出现循环,有限的二进制位无法准确的表示一个小数,这也就是小数运算出现误差的原因。
可见, 这个问题的关键点就是: “你看似有穷的小数, 在计算机的二进制表示里却是无穷的”
因此, 不要再以为这是PHP的bug了, 这就是这样的…..
PHP浮点型在进行+-*%/存在不准确的问题
2、解决方法(任意精度数学函数)
对于任意精度的数学,PHP 提供了支持用字符串表示的任意大小和精度的数字的二进制计算。
BCMath:BC 是 Binary Calculator 的缩写。
官方手册:https://www.php.net/manual/zh/book.bc.php
大家在使用前,请先确认是否已安装 BCMath,不安装使用函数会报错,下图是安装BCMath扩展,安装完成需要重启服务。

$a = 0.58;
$b = 100000;
$c = floor(bcmul($a, $b));
var_dump($c);
exit;
输出:float(58000)
bcmath 提供了以下方法:
- bcadd — 将两个高精度数字相加
- bccomp — 比较两个高精度数字,返回-1, 0, 1
- bcdiv — 将两个高精度数字相除
- bcmod — 求高精度数字余数
- bcmul — 将两个高精度数字相乘
- bcpow — 求高精度数字乘方
- bcpowmod — 求高精度数字乘方求模,数论里非常常用
- bcscale — 配置默认小数点位数,相当于就是Linux bc中的”scale=”
- bcsqrt — 求高精度数字平方根
- bcsub — 将两个高精度数字相减
3、计算原理
浮点数的处理原理就是把数字转为字符串,然后用字符串一位一位运算,再把结果拼接起来返回就行了
//就是小学时候学的手算模式
0.58(string)
+0.58(string)
_______
1.16(string)
3(string)
*0.58(string)
_______
1.74(string)
小结
通过浮点数精度的问题,了解到浮点数的小数用二进制的表示。
对于浮点数的运算,使用BCMath库来处理,数据库最好存整数,如果不能存整数,可以用decimal多存一点位数(50,15)保留15位小数,然后用BCMath运算,就可以保证万无一失了
以后,在使用浮点数运算的时候,一定要慎之又慎,细节决定成败。