PHP浮點數運算
在涉及到浮點數運算的時候,我們通常不會去深究細節,而是把它同整數運算做相同處理,認為它們和整數的區別只是多了個小數點而已。比如0.1+0.7等於0.8,我們理所當然地認為這樣的運算結果是正確的。
然鵝,看似有窮的小數, 在計算機的二進制表示里卻是無窮的。這就會導致浮點數的運算結果往往與我們的預期不符。失之毫厘,謬以千里!細節往往決定成敗。下面我們來看看浮點數運算中可能會有那些問題以及產生這些問題背后的原因。
1、浮點數運算惹的禍
1 <?php 2 3 // 加 4 $a = 0.1; 5 $b = 0.7; 6 $c = intval(($a + $b) * 10); 7 echo $c."<br>"; 8 // 輸出:7 9 10 // 減 11 $a = 100; 12 $b = 99.98; 13 $c = $a - $b; 14 echo $c."<br>"; 15 // 輸出:0.019999999999996 16 17 // 乘 18 $a = 0.58; 19 $b = 100; 20 $c = intval($a * $b); 21 echo $c."<br>"; 22 //輸出:57 23 24 // 除 25 $a = 0.7; 26 $b = 0.1; 27 $c = intval($a / $b); 28 echo $c."<br>"; 29 // 輸出:6
其實這些結果都並非語言的 bug,但和語言的實現原理有關, js 所有數字統一為 Number, 包括整形實際上全都是雙精度(double)類型。而PHP會區分 int 還是 float。不管什么語言,只要涉及浮點運算,都是存在類似的問題,使用時一定要注意。
要弄清楚浮點數運算為什么會出現這樣的結果,首先我們要知道浮點數的表示(IEEE 754)。
2、 IEEE754標准
IEEE754是20世紀80年代以來最廣泛使用的浮點數運算標准,為許多CPU與浮點運算器所采用。這個標准定義了表示浮點數的格式。
我們現在在用的計算機基本上都是基於這個標准來表示浮點數的,包括我們熟悉的短浮點數(float)、長浮點數(double),它們倆的表示方法相同,區別僅僅是階碼E和位數M的位數不同。
1)浮點數的基本定義
浮點數的形式有點像我們熟悉的科學計數法,譬如12.34這個數,可以寫成下面幾種形式:
后面這三種形式都能表示12.34這個數字,盡管它們的小數點位置各不相同,但因為后面乘了不同的10的冪次方,因此最終結果一致。
浮點數IEEE 754的標准形式:
這個式子和各個字母的含義已經非常清晰了,直接對照上面12.34這個例子看就好。當然了,12.34這個例子舉的是我們最熟悉的十進制,我們計算機中使用的當然是二進制。
其中,M為尾數,B為基數,E為階碼。B為階碼的底,E、M都用二進制數表示,M表示N的全部有效數字,E 指明小數點的位置。
如下圖:
下面逐個來看:
a、第一個位置是數符,就是表整個數字正負的符號,即0和1
b、接着是階碼E,這里的階碼也有正負,並且不用真值來表示,通常會用階碼的真值加上一個偏移量,作為實際存儲的偏移值。如在短浮點數float中,這個偏移量為127,即$2^7-1=1111111_{(2)}$
c、最后是尾數,這個部分為了提高精度,規定將原數尾數轉化為1.xxxx的形式,以1為默認最高位,然后儲存的時候並不儲存最高位1,視其為隱藏的,只存儲小數點后面的部分,這樣可以使尾數表示的精度達到最高,即存儲位數最多,比實際位數多一位。
我們再來看一下短浮點數(float)和長浮點數(double)在IEEE 754中各個部分的位數:
3、浮點數無法精確到最后一位
PHP 官方手冊解釋如下:
浮點數的精度有限。盡管取決於系統,PHP 通常使用 IEEE 754 雙精度格式,則由於取整而導致的最大相對誤差為 1.11e-16。非基本數學運算可能會給出更大誤差,並且要考慮到進行復合運算時的誤差傳遞。永遠不要相信浮點數結果精確到了最后一位,也永遠不要比較兩個浮點數是否相等。如果確實需要更高的精度,應該使用任意精度數學函數或者 gmp 函數。
這里的關鍵在於,浮點數的小數用二進制的表示,轉換過程如下:
a. 將小數乘以2,取整數部分表示第一位;
b. 將小數部分乘以2,取整數部分表示第二位;
c. 再將小數部分乘以2,取整數部分表示第三位;
... 依次類推,直到小數部分為0;
例:0.58
0.58 * 2 = 1.16 ---> 1
0.16 * 2 = 0.32 ---> 0
0.32 * 2 = 0.64 ---> 0
0.64 * 2 = 1.28 ---> 1
0.28 * 2 = 0.56 ---> 0
0.56 * 2 = 1.12 ---> 1
0.12 * 2 = 0.24 ---> 0
0.24 * 2 = 0.48 ---> 0
0.48 * 2 = 0.96 ---> 0
0.96 * 2 = 1.92 ---> 1
我們會得到一個無限循環的二進制小數:
0.1001010001...
小數部分出現循環,有限的二進制位無法准確的表示一個小數,這也就是小數運算出現誤差的原因。
1 // 0.58 對於二進制表示來說, 是無限長的值(下面的數字省掉了隱含的1).. 2 3 // 0.58的二進制表示基本上(53位)是: 4 0010100011110101110000101000111101011100001010001111 5 // 0.57的二進制表示基本上(53位)是: 6 0010001111010111000010100011110101110000101000111101 7 8 // 而兩者的二進制, 如果只是通過這53位計算的話,分別是: 9 10 0.58 -> 0.57999999999999996 11 0.57 -> 0.56999999999999995
我們就模糊的以心算來看... 0.58 * 100 = 57.999999999
再intval一下, 自然就是57了....
4、防坑攻略
1) 通過乘100的方式轉化為整數加減,然后在除以100轉化回來……
2) 使用number_format轉化成字符串,然后在使用(float)強轉回來……
3) php提供了高精度計算的函數庫,實際上就是為了解決這個浮點數計算問題而生的。
4、任意精度數學函數
對於任意精度的數學,PHP 提供了支持用字符串表示的任意大小和精度的數字的二進制計算。
BCMath:BC 是 Binary Calculator 的縮寫。
官方手冊:http://php.net/manual/zh/book.bc.php
大家在使用前,請先確認是否已安裝 bcmath。
1 <?php 2 3 // 加 4 $a = 0.1; 5 $b = 0.7; 6 $c = intval(bcadd($a, $b, 1) * 10); 7 echo $c."<br>"; 8 // 輸出:8 9 10 // 減 11 $a = 100; 12 $b = 99.98; 13 $c = bcsub($a, $b, 2); 14 echo $c."<br>"; 15 // 輸出:0.02 16 17 // 乘 18 $a = 0.58; 19 $b = 100; 20 $c = intval(bcmul($a, $b)); 21 echo $c."<br>"; 22 // 輸出:58 23 24 // 除 25 $a = 0.7; 26 $b = 0.1; 27 $c = intval(bcdiv($a, $b)); 28 echo $c."<br>"; 29 // 輸出:7
除了加減乘除,bcmath 還提供了以下方法:
- bccomp 比較兩個任意精度的數字
- bcmod 對一個任意精度數字取模
- bcpow 任意精度數字的乘方
- bcpowmod 高精度數字乘方求模
- bcscale 設置所有bc數學函數的默認小數點保留位數
- bcsqrt 任意精度數字的二次方根
下面把 常用的BC函數封裝下:

1 <?php 2 3 /** 4 * BC Math 函數示例 5 * Class BCCalculate 6 */ 7 class BCCalculate 8 { 9 private $leftNumber;// 左操作數 10 private $rightNumber;// 右操作數 11 12 public function __construct($leftNumber, $rightNumber) 13 { 14 $this->leftNumber = $leftNumber; 15 $this->rightNumber = $rightNumber; 16 $this->setScale(); 17 } 18 19 /** 20 * 設置數字 21 * @param $name 22 * @param $value 23 * @return null 24 */ 25 public function __set($name, $value) 26 { 27 if (!isset($this->$name)) { 28 return null; 29 } 30 31 $this->$name = $value; 32 } 33 34 /** 35 * 獲取數字 36 * @param $name 37 * @return null 38 */ 39 public function __get($name) 40 { 41 if (isset($this->$name)) { 42 return $this->$name; 43 } else { 44 return null; 45 } 46 } 47 48 /** 49 * 執行方法 50 * @param $functionName 51 * @param string $arguments 52 * @return null 53 */ 54 public function __call($functionName, $arguments) 55 { 56 if (!method_exists($this, $functionName)) { 57 return null; 58 } 59 60 // 設置小數點位數需要參數,其他不需要 61 if (isset($arguments[0])) { 62 return $this->$functionName($arguments[0]); 63 } 64 65 return $this->$functionName(); 66 } 67 68 /** 69 * 設置所有bc數學函數的默認小數點保留位數 70 * http://php.net/manual/zh/function.bcscale.php 71 * @param int $scale 72 */ 73 private function setScale($scale = 2) 74 { 75 bcscale($scale); 76 } 77 78 79 /** 80 * 2個任意精度數字的加法計算 81 * http://php.net/manual/zh/function.bcadd.php 82 * @return string 83 */ 84 private function add() 85 { 86 return bcadd($this->leftNumber, $this->rightNumber); 87 } 88 89 /** 90 * 2個任意精度數字的減法 91 * http://php.net/manual/zh/function.bcsub.php 92 * @return string 93 */ 94 private function sub() 95 { 96 return bcsub($this->leftNumber, $this->rightNumber); 97 } 98 99 /** 100 * 2個任意精度數字乘法計算 101 * http://php.net/manual/zh/function.bcmul.php 102 * @return string 103 */ 104 private function mul() 105 { 106 return bcmul($this->leftNumber, $this->rightNumber); 107 } 108 109 /** 110 * 2個任意精度的數字除法計算 111 * http://php.net/manual/zh/function.bcdiv.php 112 * @return string 113 */ 114 private function div() 115 { 116 return bcdiv($this->leftNumber, $this->rightNumber); 117 } 118 119 /** 120 * 比較兩個任意精度的數字 121 * 相等返回 0 ;左大於右返回 1 ;右大於左返回 -1 122 * http://php.net/manual/zh/function.bccomp.php 123 * @return int 124 */ 125 private function comp() 126 { 127 return bccomp($this->leftNumber, $this->rightNumber); 128 } 129 130 /** 131 * 對一個任意精度數字取模 132 * http://php.net/manual/zh/function.bcmod.php 133 * @return string 134 */ 135 private function mod() 136 { 137 return bcmod($this->leftNumber, $this->rightNumber); 138 } 139 140 /** 141 * 任意精度數字的乘方 142 * http://php.net/manual/zh/function.bcpow.php 143 * @return string 144 */ 145 private function pow() 146 { 147 return bcpow($this->leftNumber, $this->rightNumber); 148 } 149 150 /** 151 * 任意精度數字的二次方根 152 * http://php.net/manual/zh/function.bcsqrt.php 153 * @return string 154 */ 155 private function sqrt() 156 { 157 return bcsqrt($this->leftNumber); 158 } 159 } 160 161 $bc = new BCCalculate(3.45, 5.61); 162 163 var_dump($bc->leftNumber);// 獲取數字 float(3.45) 164 echo '<br />'; 165 $bc->leftNumber = 24.08; 166 var_dump($bc->leftNumber);// 修改數字 float(24.08) 167 echo '<br />'; 168 var_dump($bc->add());// 注意返回值是字符串 string(5) "29.69" 169 echo '<br />'; 170 $bc->setScale(3);// 修改小數點后位數 171 var_dump($bc->sub());// string(6) "18.470" 172 echo '<br />'; 173 var_dump($bc->mul());// string(7) "135.088" 174 echo '<br />'; 175 var_dump($bc->div());// string(5) "4.292" 176 echo '<br />'; 177 var_dump($bc->comp());// int(1) 178 echo '<br />'; 179 $bc->leftNumber = 10; 180 $bc->rightNumber = 4; 181 var_dump($bc->mod());// string(1) "2" 182 echo '<br />'; 183 var_dump($bc->pow());// string(5) "10000" 184 echo '<br />'; 185 $bc->leftNumber = 16; 186 var_dump($bc->sqrt());// string(5) "4.000" 187 echo '<br />';
5、擴展-MySQL 浮點型字段
在 MySQL 中,創建表字段時也有浮點數類型。
浮點數類型包括單精度浮點數(float)和雙精度浮點數(double)。
同理,不建議使用浮點數類型!!!
浮點數存在誤差,當我們使用精度敏感的數據時,應該使用定點數(decimal)進行存儲。
參考鏈接:
https://segmentfault.com/a/1190000024485146
https://cloud.tencent.com/developer/article/1437501