都知道关于浮点数的问题,处理不好,很容易程序“翻车”。其实几乎所有语言的浮点数计算都会出现问题,看似是bug,实际上并不是。
案例如下
$a = '1754.40';
$b = 73.10 * 24;
$c = 17.544 * 100;
$d = 73.1 * 24;
echo '$a = \'1754.40\'; // '; var_dump($a);
echo '<br>';
echo '$b = 73.10 * 24; // '; var_dump($b);
echo '<br>';
echo '$c = 17.544 * 100; // '; var_dump($c);
echo '<br>';
echo '$d = 73.1 * 24; // '; var_dump($d);
echo '<hr>比较结果如下:<hr>';
if($a == $b){
echo '$a 等于 $b';
}else{
echo '$a 不等于 $b';
}
if($a == $c){
echo '<br>';
echo '$a 等于 $c';
}else{
echo '<br>';
echo '$a 不等于 $c';
}
if($a == $d){
echo '<br>';
echo '$a 等于 $d';
}else{
echo '<br>';
echo '$a 不等于 $d';
}
if($b == $c){
echo '<br>';
echo '$b 等于 $c';
}else{
echo '<br>';
echo '$b 不等于 $c';
}
if($b == $d){
echo '<br>';
echo '$b 等于 $d';
}else{
echo '<br>';
echo '$b 不等于 $d';
}
if($c == $d){
echo '<br>';
echo '$c 等于 $d';
}else{
echo '<br>';
echo '$c 不等于 $d';
}
以上代码运行结果如下:
$a = '1754.40'; // string(7) "1754.40"
$b = 73.10 * 24; // float(1754.4)
$c = 17.544 * 100; // float(1754.4)
$d = 73.1 * 24; // float(1754.4)
比较结果如下:
$a 不等于 $b
$a 等于 $c
$a 不等于 $d
$b 不等于 $c
$b 等于 $d
$c 不等于 $d
那么在只比较值的情况下,为何 $c 不等于 $d 呢?
简化下上述代码,例子如下:
$a = 0.1;
$b = 0.7;
$c = $a + $b;
$d = 0.8;
echo '$a = 0.1; // '; var_dump($a);
echo '<br>';
echo '$b = 0.7; // '; var_dump($b);
echo '<br>';
echo '$c = $a + $b; // '; var_dump($c);
echo '<br>';
echo '$d = 0.8; // '; var_dump($d);
echo '<hr>比较结果如下:<hr>';
if($c == $d){
echo '$c 等于 $d';
}else{
echo '$c 不等于 $d';
}
以上代码运行结果如下:
$a = 0.1; // float(0.1)
$b = 0.7; // float(0.7)
$c = $a + $b; // float(0.8)
$d = 0.8; // float(0.8)
比较结果如下:
$c 不等于 $d
问题就很明显了,同样是 float(0.8) ,为何不等呢?别急,我们再变化下代码。
$a = 0.9 - (0.3 + 0.6);
// $a 打印的结果是 1.1102230246252E-16
echo '运行结果为:',$a;
当我们在尝试多种浮点数加减的时候,就会发现并不是所有的情况都会出现偏差,那么究竟是为什么呢?我们又该如何处理精度问题。究其原因是二进制转换问题。这里有一篇关于小数如何转化成二进制的详细理解的文章,需要的可以查看下。
PHP 通常使用 IEEE 754 双精度格式,关于浮点数的PHP官方文档。
顺便先抛出来文档中的一句话:永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数或者 gmp 函数。
0.3的二进制为,只取64位
0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100
再将二进制转换成十进制:0.296875
// 0 * pow(2, -1) + 1 * pow(2, -2) + 0 * pow(2, -3) + 0 * pow(2, -4) + 1 * pow(2, -5) + 1 * pow(2, -6) + 0 * pow(2, -7) + 0 * pow(2, -8);
0.6的二进制为,只取64位
1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001
再将二进制转换成十进制:0.59765625
// 1 * pow(2, -1) + 0 * pow(2, -2) + 0 * pow(2, -3) + 1 * pow(2, -4) + 1 * pow(2, -5) + 0 * pow(2, -6) + 0 * pow(2, -7) + 1 * pow(2, -8);
$a 和 $b 的十进制和为 0.89453125,结果当然就不等于 0.9了。
理论上讲在逻辑代码中处理无限小数时是没有很好的方法的,最好的方式是所有的浮点数尽可能转换成整形数然后再处理,或者每次运算都是用高精度的函数运算,然而不得不说的是,这一切也是建立在有限可知的小数位才能实现。
常用的高精度函数如下:
bcadd — 加法
bccomp — 比较
bcdiv — 相除
bcmod — 求余数
bcmul — 乘法
bcpow — 次方
bcpowmod — 先次方然后求余数
bcscale — 给所有函数设置小数位精度
bcsqrt — 求平方根
bcsub — 减法