在用PHP進行浮點數的運算中,遇到一個坑,沒有得到預期中的結果,如下代碼:
$a = 69.1;
$b = $a*100;
$c = $b-6910;
你猜$c的值是多少?$c輸出的值是-9.0949470177293E-13.為什么會這樣?
在PHP官網Float浮點型頁面中,講到:
浮點數的精度
浮點數的精度有限。盡管取決于系統,PHP 通常使用 IEEE 754 雙精度格式,則由于取整而導致的最大相對誤差為 1.11e-16。非基本數學運算可能會給出更大誤差,并且要考慮到進行復合運算時的誤差傳遞。
此外,以十進制能夠精確表示的有理數如 0.1 或 0.7,無論有多少尾數都不能被內部所使用的二進制精確表示,因此不能在不丟失一點點精度的情況下轉換為二進制的格式。這就會造成混亂的結果:例如,floor((0.1+0.7)*10) 通常會返回 7 而不是預期中的 8,因為該結果內部的表示其實是類似 7.9999999999999991118…。
所以永遠不要相信浮點數結果精確到了最后一位,也永遠不要比較兩個浮點數是否相等。如果確實需要更高的精度,應該使用任意精度數學函數或者gmp函數。
那么如何正確處理PHP浮點數計算有誤的問題呢?
- $x = 8 - 6.4; // which is equal to 1.6
- $y = 1.6;
- var_dump($x == $y); // is not true
以上例子中$x和$y的值并不等。解決辦法是用round()函數,如:
var_dump(round($x, 2) == round($y, 2)); // this is true
問題的原因在于$x并不是1.6,而是1.599999.
所以本文開頭的例子改成下面這樣就OK了:
- $a = 69.1;
- $b = $a*100;
- $c = round($b)-6910;
或者使用number_format((float)$a, 2)格式化。
再看一個關于PHP浮點數算出來結果不符合預期的例子。
- $a = intval( 0.58*100 );
- $b = 0.58*100;
$a的值出乎意料的是57,$b的值是58.
解決辦法是:
$a = intval( (0.58*1000)/10 );
或者使用Binary Calculator,即BCMath擴展解決以上問題
補充:
- <?php
- $f = 0.58;
- var_dump(intval($f * 100)); //為啥輸出57
- ?>
為啥輸出是57啊? PHP的bug么?
我相信有很多的同學有過這樣的疑問, 因為光問我類似問題的人就很多, 更不用說bugs.php.net上經常有人問…
要搞明白這個原因, 首先我們要知道浮點數的表示(IEEE 754):
浮點數, 以64位的長度(雙精度)為例, 會采用1位符號位(E), 11指數位(Q), 52位尾數(M)表示(一共64位).
符號位:最高位表示數據的正負,0表示正數,1表示負數。
指數位:表示數據以2為底的冪,指數采用偏移碼表示
尾數:表示數據小數點后的有效數字.
這里的關鍵點就在于, 小數在二進制的表示, 關于小數如何用二進制表示, 大家可以百度一下, 我這里就不再贅述, 我們關鍵的要了解, 0.58 對于二進制表示來說, 是無限長的值(下面的數字省掉了隱含的1)..
0.58的二進制表示基本上(52位)是: 0010100011110101110000101000111101011100001010001111
0.57的二進制表示基本上(52位)是: 0010001111010111000010100011110101110000101000111101
而兩者的二進制, 如果只是通過這52位計算的話,分別是:
0.58 -> 0.57999999999999996
0.57 -> 0.56999999999999995
至于0.58 * 100的具體浮點數乘法, 我們不考慮那么細, 有興趣的可以看(Floating point), 我們就模糊的以心算來看… 0.58 * 100 = 57.999999999
那你intval一下, 自然就是57了….
可見, 這個問題的關鍵點就是: “你看似有窮的小數, 在計算機的二進制表示里卻是無窮的”
so, 不要再以為這是PHP的bug了, 這就是這樣的…..
新聞熱點
疑難解答