对于这个问题的发现源于一个计时器程序,先看下这段代码吧。
$s=gettime();
usleep(10000);
$e=gettime();
var_dump($s);
var_dump($e);
echo $e-$s."\n";
function gettime(){
list($sec,$usec)=explode(" ",microtime());
return $sec+$usec;
}
?>
输出结果为:
float(1296098836.2033)
float(1296098836.2135)
0.010126113891602
有没有发现一个很诡异的现象?被减数和减数整数部分是相同的,小数点后都只是四位,但是这两个数相减之后得出的结果,小数点后的位数却要大于4。其实想解释也不难,因为被减数和减数的精度都为14,所以为不造成精度损失,所以结果的精度也必须是14位的。
但出现这种现象的根本原因在哪?
所以我去网上逛了逛,看了些东西后,得到了一些启发。所以又写了一段测试代码:
ini_set('precision',14);
$a=0.3;
$b=0.1+0.2;
var_dump($a);
var_dump($b);
$a==$b?print "equals\n":print "not equals\n";
?>
输出的结果是:
float(0.3);
float(0.3);
not equals
大家是不是感觉很奇怪,两个变量都是浮点型的,都是0.3,为什么不相等呢?
使用序列化函数serialize查看一下两个数的实际值:
echo serialze(0.3),"\n";
echo serialize(0.1+0.2),"\n";
?>
输出结果:
d:0.299999999999999988897769753748434595763683319091796875;
d:0.3000000000000000444089209850062616169452667236328125;
你会发现这两个数实际上都不是真正的0.3,为什么这样呢?
其实这个问题要追溯到微机原理(也有可能是计算机组成原理,记不清楚是哪一本书了),这里面讲了计算机是如何用二进制来存储定点小数。大致是这样 的:如果用一个字节的长度来表示一个定点小数,第一位表示小数的符号,0为正,1为负;后面7位表示小数的值,第2位至第8位的位权分别是 1/2,1/4,1/8,1/16,1/32,1/64,1/128,然后用这些权值的和来表示所有的小数。如何表示0.625呢?
这个有固定的算法:首先,将小数点左侧的整数部分变换为其二进制形式,处理小数部分的算法是将我们的小数部分乘以基数 2,记录乘积结果的整数部分,接着将结果的小数部分继续乘以 2,并不断继续该过程。
0.625 x 2 = 1.25 1
0.25 x 2 = 0.5 0
0.5 x 2 = 1 1
当最后的结果为1时,结束这个过程。这时右侧的一列数字就是我们所需的二进制小数部分,即 0.101。这样,我们就得到了完整的二进制形式 0.101 ,按阶展开:(1*(1/2))+(0*(1/4))+(1*(1/8))=0.5+0.125=0.625。
我们上面先的例子比较特殊,这个数只需要三次运算就能够结束。那会不会有无法结束的情况,不难想象,很多小数根本不能经过有限次这样的过程而得到结 果(比如最简单的 0.1)。但浮点数尾数域的位数是有限的,为此,浮点数的处理办法是持续该过程直到由此得到的尾数足以填满尾数域,之后对多余的位进行舍入。也就是说,十 进制到二进制的变换也并不能保证总是精确的,而只能是近似值。事实上,只有很少一部分十进制小数具有精确的二进制浮点数表达。再加上浮点数运算过程中的误 差累积,结果是很多我们看来非常简单的十进制运算在计算机上却往往出人意料。这就是最常见的浮点运算的"不准确"问题。
所以,在计算机里表示的浮点数只是一个近似的数,并不是像表示整数那样精确没有偏差。既然它只是一个约数,那么你用精确的==来比较两位不精确的约数就没有太大意义了。如果一定要比较两个浮点数,可以考虑先转换成字符串,然后再去比较。
我在Linux下使用c编写了一段类似的浮点数比较的代码,结果也是不相等的。实际上在所有的语言里都会存在此问题,因为这是计算机原理所决定的。但也不排除一些语言做了些后期处理,可以直接比较两个浮点数。
以上仅是个人理解,不保证绝对正确,有错误还请多多谅解。