題目
0.1 + 0.2 是否等於 0.3 ?
解答
首先直接通過瀏覽器開發者模式打開控制檯看一下結果。
其實看到這個題目應該就可以猜到肯定不可能是 0.3,否則就不會出這個題目了。這個題目涉及到數學運算中的浮點運算。
0.1 轉二進制
0.1 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + …
左右兩邊一直乘以2,將小數與正數分開,得到以下結果。
0 + 0.2 = a * 2^0 + b * 2^-1 + c * 2^-2 + … (a = 0)
0 + 0.4 = b * 2^0 + c * 2^-1 + d * 2^-2 + … (b = 0)
0 + 0.8 = c * 2^0 + d * 2^-1 + e * 2^-2 + … (c = 0)
1 + 0.6 = d * 2^0 + e * 2^-1 + f * 2^-2 + … (d = 1)
1 + 0.2 = e * 2^0 + f * 2^-1 + g * 2^-2 + … (e = 1)
0 + 0.4 = f * 2^0 + g * 2^-1 + h * 2^-2 + … (f = 0)
0 + 0.8 = g * 2^0 + h * 2^-1 + i * 2^-2 + … (g = 0)
1 + 0.6 = h * 2^0 + i * 2^-1 + j * 2^-2 + … (h = 1)
…
這個計算在不停的循環,所以 0.1 用二進制表示就是 0.00011001100110011……
0.2 轉二進制
0.2 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + …
左右兩邊一直乘以2,將小數與正數分開,得到以下結果。
0 + 0.4 = a * 2^0 + b * 2^-1 + c * 2^-2 + … (a = 0)
0 + 0.8 = b * 2^0 + c * 2^-1 + d * 2^-2 + … (b = 0)
1 + 0.6 = c * 2^0 + d * 2^-1 + e * 2^-2 + … (c = 1)
1 + 0.2 = d * 2^0 + e * 2^-1 + f * 2^-2 + … (d = 1)
0 + 0.4 = e * 2^0 + f * 2^-1 + g * 2^-2 + … (e = 0)
0 + 0.8 = f * 2^0 + g * 2^-1 + h * 2^-2 + … (f = 0)
1 + 0.6 = g * 2^0 + h * 2^-1 + i * 2^-2 + … (g = 1)
1 + 0.2 = h * 2^0 + i * 2^-1 + j * 2^-2 + … (h = 1)
…
0.2 用二進制表示就是 0.00110011001100110……
浮點數
-
整數型存儲整數,浮點型存儲小數。顯示浮點數的方法有兩種,單精度和雙精度。單精度用 32 位表示,雙精度用 64 位表示。JavaScript 是遵循國際 IEEE 754 標準,將數字存儲爲雙精度浮點數,也就是用 64 位表示。
-
最大數和最小數一般用科學計數法來表示。比如 0.000045 可以表示爲 0.45 * 10^4。某市有 10000000 人口可以表示爲 1 * 10 ^7。科學記數法主要是爲了書寫方便。
-
對於二進制也是一樣,以 0.1 的二進制 0.00011001100110011…… (後面有0.1轉二進制的過程)這個數來說:
可以表示爲:1 * 2^-4 * 1.1001100110011……
二進制科學計數法公式爲:V = (-1)^S (1 + Fraction) 2^E -
所有浮點數都可以用以上公式來表示,所以我們只需要把變化的 S,Fraction 以及 E 存儲即可。以 64 位存儲爲例,其中用 1 位存儲 S(sign,存在位 63 上,表示符號位,取 0 表示正數,1表示負數)。用 11 位存儲 E+bias,存在位 52-62 上。用 52 位存儲 Fraction,存在位 0-51 上。
綜上所述,上文 1 * 2^-4 * 1.1001100110011…… 例子中 ,
Sign(該數爲正數)爲 0。
Fraction(小數部分)爲 1001100110011……。
Exponent(指數)爲 -4。
bias (這裏是爲了輔助存儲 E,因爲要存11位,如果只是正數的話是 2^11 -1 = 2047,範圍是 0 - 2046。但是可能存在負數,所以取值範圍爲 -1023-1023,爲了不存負數,存儲的時候需要加 1023( 2^(11-1)-1=1023),取值的時候再減去1023)。所以 E+bias = -4+1023 = 1019。而 1019 的二進制爲 1111111011。
所以 0.1 用 64 位二進制表示如下:
0 01111111011 1001100110011001100110011001100110011001100110011010
同理,0.2 用 64 位二進制表示如下:
0 01111111100 1001100110011001100110011001100110011001100110011010
浮點數運算
浮點數運算有五個步驟:對階、尾數運算、規格化、舍入處理、溢出判斷。
-
對階就是把階碼對齊,其目的是爲了使兩個浮點數的尾數進行加減運算。比如 0.1 的二進制科學記數法是
1.1001100110011…… * 2^-4
,階碼就是 -4。而 0.2 的二進制科學記數法是1.10011001100110...* 2^-3
,階碼就是 -3,兩個階碼不同,所以先調整爲相同的階碼再進行計算,調整原則是小階對大階,同時將小階碼對應的浮點數的尾數右移相應位數,以保證該浮點數的值不變。也就是 0.1 的 -4 調整爲 -3,對應變成 0.11001100110011…… * 2^-3 -
尾數運算
0.1100110011001100110011001100110011001100110011001101
+
1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————
10.0110011001100110011001100110011001100110011001100111 -
規格化
將這個結果處理一下,即結果規格化,變成 1.0011001100110011001100110011001100110011001100110011(1) * 2^-2 -
舍入處理
括號裏的 1 是計算後這個 1 超出了範圍,也就是超出了 52 位,所以要捨棄。四捨五入對應到二進制中,就是 0 舍 1 入,因爲要把括號裏的 1 丟了,這裏會進 1,結果變成 1.0011001100110011001100110011001100110011001100110100 * 2^-2PS:這裏不涉及溢出判斷。
所以最終的結果存成 64 位就是
0 01111111101 0011001100110011001100110011001100110011001100110100
將它轉換爲 10 進制數就得到 0.30000000000000004440892098500626
因爲兩次存儲時的精度丟失加上一次運算時的精度丟失,最終導致了 0.1 + 0.2 !== 0.3
總結
- 小數轉二進制可能有些麻煩,需要兩邊不斷乘2取0或1。
- 用科學計數法時,我們只需要存儲 S、Fraction、E(E+bais)。
- 用 64 位表示的二進制中,其中1位表示符號位,11位表示指數位(注意這裏是 E+bias),52位表示小數位。