Php中浮点数计算问题

来自技术开发小组内部wiki
跳转至: 导航搜索

现在为大家讲一讲平常存在却又不易发现的浮点数问题

大家都知道 0.7+0.2=0.9 、 10000.30+2000.30+299.40=12300.00 、 intval( 0.58*100 )=58 ......,那他们真的是等于那个吗?

不知道大家平常有没有发现,拿0.7+0.2来说吧,var_dump(0.7+0.2 == 0.9)大家会觉得出来的是true还是false?

不要再凭借大家的想象力了,偏偏出来的结果不会按照想象的结果走,实际结果居然是false,为什么,0.7+0.2不就是等于0.9吗?下面我为大家分析一下浮点数的问题。

1、浮点数计算结果比较

一则浮点数计算例子如下:
$a = 0.2+0.7;
$b = 0.9;
var_dump($a == $b);
对此问题,PHP官方手册曾又说明:显然简单的十进制分数如 0.2 不能在不丢失一点点精度的情况下转换为内部二进制的格式。

这和一个事实有关,那就是不可能精确的用有限位数表达某些十进制分数。例如,十进制的 1/3 变成了 0.3333333...。

我们将上面的变量用双精度格式打印出来:
$a = 0.2+0.7;
$b = 0.9;
printf("%0.20f", $a);
echo '<br />';
printf("%0.20f", $b);
输出结果如下:
0.89999999999999991118
0.90000000000000002220
显然在这里,实际上作为浮点型数据,其精度已经损失了一部分,达不到完全精确。所以永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。需要说明的是,这不是PHP的问题,而是计算机内部处理浮点数的问题!在 C、JAVA 等语言中也会遇到同样的问题。 所以要比较两个浮点数,需要将其控制在我们需要的精度范围内再行比较,因此使用 bcadd() 函数来对浮点数想加并进行精度转换(为字符串,后面会介绍高精度函数的用法):
var_dump(bcadd(0.2,0.7,1) == 0.9); // 输出:bool(true) 
2、浮点数取整

大家都知道echo 2.1/0.7; 打印出来的结果是3,;没错,是3,通过上面的例子大家应该也都清楚如果和3直接进行比较的话结果是false,不相等,原因不再细说,这里要说的是ceil(2.1/0.7),按照常规,结果应该是3,

可出来的结果却是4,为什么呢?经过printf('%0.20f',2.1/0.7)得到的结果是3.00000000000000044409;没错,ceil(3.00000000000000044409)结果肯定是4(ceil不懂的查一下)。
echo ceil(2.1/0.7); // 输出:4
那如何要得到我们想要的结果呢?

经过上面对浮点数计算的探讨,知道这是浮点数计算结果不完全精确造成的,因此使用 round()函数处理一下即可:

echo ceil( round(2.1/0.7,1) );

没错,这样就能得到你想要的结果3。先将浮点数四舍五入再用ceil

在小数计算比较时除了round外我们还可以用number_format等任意精度 gmp函数或者数学函数计算等方法。

3、GMP函数

GMP是The GNU MP Bignum Library,是一个开源的数学运算库,它可以用于任意精度的数学运算,包括有符号整数、有理数和浮点数。它本身并没有精度限制,只取决于机器的硬件情况。

本函数库能处理的数值范围只到长整数与倍浮点数的范围。若要处理超过上述范围的数值,要使用 bc 高精确度函数库 。本函数库定义了圆周率的常量 m_pi 值为 3.14159265358979323846。

GMP函数包括

abs: 取得绝对值。 acos: 取得反余弦值。 asin: 取得反正弦值。 atan: 取得反正切值。 atan2: 计算二数的反正切值。 base_convert: 转换数字的进位方式。 bindec: 二进位转成十进位。 ceil: 计算大于指定数的最小整数。 cos: 余弦计算。 decbin: 十进位转二进位。 dechex: 十进位转十六进位。 decoct: 十进位转八进位。 exp: 自然对数 e 的次方值。 floor: 计算小于指定数的最大整数。 getrandmax: 随机数的最大值。 hexdec: 十六进位转十进位。 log: 自然对数值。 log10: 10 基底的对数值。 max: 取得最大值。 min: 取得最小值。 mt_rand: 取得随机数值。 mt_srand: 配置随机数种子。 mt_getrandmax: 随机数的最大值。 number_format: 格式化数字字符串。 octdec: 八进位转十进位。 pi: 圆周率。 pow: 次方。 rand: 取得随机数值。 round: 四舍五入。 sin: 正弦计算。 sqrt: 开平方根。 srand: 配置随机数种子。 tan: 正切计算。

因有的大家一直在用,顾这里不再做多的解释,大家可以自己动手实现下。下面主要讲解下bc高精度函数:

4、bc高精度函数

bcadd — 将两个高精度数字相加
bccomp — 比较两个高精度数字,返回-1, 0, 1
bcdiv — 将两个高精度数字相除
bcmod — 求高精度数字余数
bcmul — 将两个高精度数字相乘
bcpow — 求高精度数字乘方
bcpowmod — 求高精度数字乘方求模,数论里非常常用
bcscale — 配置默认小数点位数,相当于就是Linux bc中的”scale=”
bcsqrt — 求高精度数字平方根
bcsub — 将两个高精度数字相减 整理了一些实例

php BC高精确度函数库包含了:相加,比较,相除,相减,求余,相乘,n次方,配置默认小数点数目,求平方。这些函数在涉及到有关金钱计算时比较有用,比如电商的价格计算。

注意点:关于设置的位数,超出部分是丢弃掉,而不是四舍五入。

/**
  * 两个高精度数比较
  *
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精确到的小数点位数
  *
  * @return int $left==$right 返回 0 | $left<$right 返回 -1 | $left>$right 返回 1
  */
var_dump(bccomp($left=4.45, $right=5.54, 2));
// -1
 
 /**
  * 两个高精度数相加
  *
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精确到的小数点位数
  *
  * @return string
  */
var_dump(bcadd($left=1.0321456, $right=0.0243456, 2));
//1.05
 
  /**
  * 两个高精度数相减
  *
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精确到的小数点位数
  *
  * @return string
  */
var_dump(bcsub($left=1.0321456, $right=3.0123456, 2));
//-1.98
 
 /**
  * 两个高精度数相除
  *
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精确到的小数点位数
  *
  * @return string
  */
var_dump(bcdiv($left=6, $right=5, 2));
//1.20
 
 /**
  * 两个高精度数相乘
  *
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精确到的小数点位数
  *
  * @return string
  */
var_dump(bcmul($left=3.1415926, $right=2.4569874566, 2));
//7.71
 
 /**
  * 设置bc函数的小数点位数
  *
  * @access global
  * @param int $scale 精确到的小数点位数
  *
  * @return void
  */
bcscale(3);
var_dump(bcdiv('105', '6.55957'));
// 16.007



上面我们说了对于浮点数计算不分语言,都会出现这种问题,原因是计算机精确问题,那javascript又是怎样的呢?

举个例子:

<script>
var a = 0.7+0.2 ;
if(a == 0.9){
    alert('相等'+a);
}else{
    alert('不相等'+a);
}
</script>

大家会觉得出现什么结果呢?没错,结果是不相等0.8999999999999999;可见对于js直接将精度打印出来,而php如果打印0.7+0.2会直接输出0.9,表面会让人迷惑,php必须用双精度函数打印出来才能看出结果

那么js要如何进行比较呢?

可以用toFixed()函数可把 Number 四舍五入为指定小数位数的数字;返回的是字符串形式

将上面的a进行处理保留一位小数

a = a.toFixed(1);

if(a == 0.9){
    alert('相等'+a);
}else{
    alert('不相等'+a);
}

这回会a出现什么样的结果呢?答案是 相等0.9

因tofixed()函数返回的是字符串形式,顾如果想要用它返回的值再计算,必须转换成浮点数,可用parseFloat();

例:上面的结果为0.9的字符串,如果不进行parseFloat()处理,alert(a+1)结果是0.91,详单与字符串拼接,如果处理,alert(parseFloat(a)+1)结果是1.9

如果需要比较两个浮点数表达式是否相同的话,也可以采取以下方法:

<code class="lang-javascript"><span class="hljs-keyword">var</span> diff = <span class="hljs-number">1e-6</span>; <span class="hljs-comment">/* 允许最大误差0.000001 */</span>
<span class="hljs-keyword">var</span> exp1 = <span class="hljs-number">0.3</span>;
<span class="hljs-keyword">var</span> exp2 = <span class="hljs-number">0.1</span> + <span class="hljs-number">0.2</span>;

<span class="hljs-built_in">console</span>.log(exp1 === exp2); <span class="hljs-comment">/* false exp2*/</span>
<span class="hljs-built_in">console</span>.log(exp1 - exp2 < diff); <span class="hljs-comment">/* true */</span>
</code>

/*exp1 - exp2的结果是-5.551115123125783e-17*/


先写到这里,后续可能会补充