目录
PHP 中浮点数运算精度丢失问题
1. 浮点数运算精度丢失的产生原因
在计算机中,只有二进制的数据才能被识别和处理。所以无论是哪种编程语言,在什么编译环境下工作,都要先把源程序(编译)转换成二进制的机器码后才能被计算机识别。***二进制的方式可以准确表示一个整数,但不能准确表示一个浮点数。***和十进制无法精确表示分数的1/3同样,二进制也无法精确表示十进制的小数。我们可以看下面的例子:
// 十进制数 134 表示为二进制
echo decbin(134); //输出:10000110
// 十进制数 184656 表示为二进制
echo decbin(184656); //输出:101101000101010000
// 但是对于浮点数来说,二进制并不能完整地表示一个浮点数。
// 例如,我们将浮点数 2.4 表示为二进制,此时不能使用 decbin(), bindec()等类似的php系统函数。这里我是用在线转换工具(https://hexconvert.vvcha.cn/),来实现对浮点数二进制表示
2.4 = 10.011001100110011001100110011001100110011001100110011;
0.8 = 0.1100110011001100110011001100110011001100110011001101;
// 2.4 的二进制表示与 0.8 的二进制表示之和
10.011001100110011001100110011001100110011001100110011
+ 0.1100110011001100110011001100110011001100110011001101;
--------------------------------------------------------------------------------
11.0011001100110011001100110011001100110011001100110011
// 将计算结果 11.0011001100110011001100110011001100110011001100110011 转为十进制表示
11.0011001100110011001100110011001100110011001100110011 = 3.19999999999999996
// 那么实际上来说,通过计算机计算的出来的结果是
2.4 + 0.8 = 3.19999999999999996
// 而不是我们常识上所认为
2.4 + 0.8 = 3.2
从上面的例子可以看出,小数表示为二进制,通过二进制运算后,在转换为十进制,会造成精度丢失的问题。
我们再来看一个例子。
var_dump(intval(0.58 * 100)); //输出:57
var_dump(0.58 * 100); //输出:58
var_dump((0.1 + 0.7) == 0.8); //输出:false
2. PHP 中解决浮点数计算精度丢失的方案
- 将小数转为整数去计算,最后在对运行结果进行处理。
function precison_restore($num1, $num2, $type) {
$ten = pow(10, maxLenOfDecimal($num1, $num2));
switch ($type) {
case '+':
$res = ($num1*$ten + $num2*$ten) / $ten;
break;
case '-':
$res = ($num1*$ten - $num2*$ten) / $ten;
break;
case '*':
$res = ($num1*$ten * $num2*$ten) / pow($ten,2);
break;
case '/':
$res = ($num1*$ten) / ($num2*$ten);
break;
default:
}
return $res;
}
function maxLenOfDecimal($num1 , $num2) {
return max(decimalLen($num1), decimalLen($num2));
}
function decimalLen($num) {
$len = 0;
if ($decimal = ltrim(strrchr($num, '.'), '.')) {
$len = strlen($decimal);
}
return $len;
}
$time_s = microtime(true);
var_dump(floor(precison_restore(2.1, 0.7, '-')*10)); // 输出:14
$time_e = microtime(true);
echo $time_e - $time_s; // 输出:7.1048736572266E-5
- 使用 php 官方提供的 BC Math 高精确度的数学扩展。(个人认为是最佳实践)
function simple($num1, $num2, $type) {
bcscale(maxLenOfDecimal($num1, $num2));
switch ($type) {
case '+':
$res = bcadd($num1, $num2);
break;
case '-':
$res = bcsub($num1, $num2);
break;
case '*':
$res = bcmul($num1, $num2);
break;
case '/':
$res = bcdiv($num1, $num2);
break;
default:
}
return $res;
}
function maxLenOfDecimal($num1 , $num2) {
return max(decimalLen($num1), decimalLen($num2));
}
function decimalLen($num) {
$len = 0;
if ($decimal = ltrim(strrchr($num, '.'), '.')) {
$len = strlen($decimal);
}
return $len;
}
$time_s = microtime(true);
var_dump(floor(simple(2.1, 0.7, '-')*10)); // 输出:14
$time_e = microtime(true);
echo $time_e - $time_s;// 输出:1.4066696166992E-5
对比来看
使用 bcmath 扩展的效率要比 将浮点数转换为整数进行计算,在对结果进行处理的方式要高效。