Why系列:0.1 + 0.2 != 0.3

爲了知道更多一點,打算自己來一個why系列。

  • 面試官:同學, 請問 0.1 + 0.2 等於多少
  • 同學:不等於0.3, 因爲精度問題
  • 面試官:能更深入的說一下嘛
  • 同學:......

上面的同學,就是曾今的我!

所以,幹!

來解決 0.1 + 0.2 這個小學生都會的題目,大致分三個步驟

  1. 進制轉換
  • 十進制轉二進制
  • 二進制轉十進制
  1. IEEE 754浮點數標準
  2. 浮點數計算
  3. 0.1 + 0.2

進制轉換

十進制轉二進制

  • 整數: 採用 除2取餘,逆序排列法。具體做法是:用2整除十進制整數,可以得到一個商和餘數;再用2去除商,又會得到一個商和餘數,如此進行,直到商爲小於1時爲止,然後把先得到的餘數作爲二進制數的低位有效位,後得到的餘數作爲二進制數的高位有效位,依次排列起來。
  • 小數: 採用乘2取整,順序排列法。具體做法是:用2乘十進制小數,可以得到積,將積的整數部分取出,再用2乘餘下的小數部分,又得到一個積,再將積的整數部分取出,如此進行,直到積中的小數部分爲零,此時0或1爲二進制的最後一位。或者達到所要求的精度爲止

我們依舊使用 9.375 來分析,

先看整數部分:9, 按照規則,除2取餘,逆序排列
結果爲: 1001

再看小數部分: 0.375 ,按照規則 採用乘2取整,順序排列
結果爲: 011

結合起來 9.375 = 整數2 + 小數2 = 1001 + .011 = 1001.011

驗證:

(9.375).toString(2)  // 1001.011
Number.prototype.toString.call(9.375, 2)  // 1001.011
Number.prototype.toString.call( Number(9.375), 2) // 1001.011

二進制轉十進制

  • 小數點前或者整數要從右到左用二進制的每個數去乘以2的相應次方並遞增
  • 小數點後則是從左往右乘以二的相應負次方並遞減。

例如:二進制數1001.011轉化成十進制

整數部分:1001 = 1 * 20 + 0 * 21 + 0 * 22 + 1 * 23 = 1 + 0 + 0 + 8 = 9
小數部分:011 = 0 * 2-1 + 1 * 2-2 + + 1 * 2-3 = 0 + 0.25 + 0.125 = 0.375

1001.0112 = 整數部分 + 小數部分 = 910 + 0.375 10 = 9.375

當然我們怎麼驗證結果了,調用Number.prototype.toString

(9.375).toString(2)  // 1001.011
Number.prototype.toString.call(9.375, 2)  // 1001.011
Number.prototype.toString.call( Number(9.375), 2) // 1001.011

IEEE 754浮點數標準


以雙精度浮點格式爲例,如上圖,三個參數 S E M:

名稱                        長度        比特位置

符號位    Sign  (S)      : 1bit        (b63)
指數部分Exponent (E)     : 11bit      (b62-b52)
尾數部分Mantissa   (M)   : 52bit      (b51-b0)

雙精度的指數部分(E)採用的偏置碼爲1023

S=1表示負數 S=0表示正數

求值公式 (-1)^S*(1.M)*2^(E-1023)
這裏有一個額外的參數,偏移碼 1023, 更多可以參考IEEE 754浮點數標準中64位浮點數爲什麼指數偏移量是1023
結合公司來看, 各個參數是怎麼工作的:
二進制可能不好理解,我們先看一個10進制的數, 比如 1001.125, 我們可以寫成

  • 1001.125
  • 100.1125*101
  • 10.01125*102
  • 1.001125*103

上面的(-1)^S*(1.M)*2^(E-1023) 同 1.001125*103 只不過使用的是二進制而已。
如果是小數了,同理 0.0000125 就是 1.25 * 10-5

我們來看看一個二進制的例子, 比如 103.0625

  • S 符號位
    因爲正數,所以 S爲0
  • E 指數位
    1. 轉爲二進制爲 1100111.0001
    2. 規範化 1.1001110001 * 26, 6 = E - 1023 , E = 1029
    3. E = 1029 , 轉爲二進制 10000000101
  • M 尾數位
    對於 1.1001110001 , M的值爲 1001110001, 因爲長度有52位,後面補充0就行, 結果爲
    1001110001000000000000000000000000000000000000000000

拼接來 0-10000000101-1001110001000000000000000000000000000000000000000000

至於如何驗證對不對,可以去IEEE 754 64位轉換工具 驗證一下。

大家知道,十進制有無限循壞小數,二進制也是存在的。遇到這種情況,10進制是四捨五入,那二進制呢。 只有0和1,那麼是1就入吧。
當然 IEEE 754 是有好幾種舍入誤差的模式的,更多細節可以閱讀 IEEE 754浮點數標準詳解

我們看看 1.1, 最後尾數部分 000110011001100110011001100110011001100110011001100152 (11001)循環 ,53位是1,那就進位吧,
結果爲 0001100110011001100110011001100110011001100110011010

這裏知道十進制,怎麼轉換爲二進制的浮點數存儲了,下面就可以進行運算。

浮點數計算

浮點數的加減運算一般由以下五個步驟完成:對階、尾數運算、規格化、舍入處理、溢出判斷。 更多細節,可以閱讀浮點數的運算步驟

1.對階

這裏的階,就是指數位數,簡單說,就是指數位保持一致。 即⊿E=E x-E y,將小階碼加上⊿E,使之與大階碼相等。
拿是十進制舉例, 123.5 + 1426.00456

  • 等價於 1.235*102 + 1.42600456 * 103
  • 和指數高的對齊,高的爲3 ,變成 0.1235*103 + 1.42600456 * 103

123.5 + 1426.00456 = 0.1235*103 + 1.42600456 * 103 = (0.125 + 1.42600456) * 103 = 1.54950456 * 103 = 1549.50456

舉例是十進制, 計算機執行的是二進制而已。 這個過程可能會有幾個問題。

  • 小階對大階的時候,會右移動, 因爲指數部分,最多保留52位,就可能丟。
  • 相加或者相減,值可能溢出,就有了後面的溢出判斷。

2.尾數運算

尾數運算就是進行完成對階後的尾數相加減。

3.結果規格化

在機器中,爲保證浮點數表示的唯一性,浮點數在機器中都是以規格化形式存儲的。對於IEEE754標準的浮點數來說,就是尾數必須是1.M的形式。
再拿十進制舉例。

80.5 + 90 = 8.05*101 + 9.0*101 = 17.051
尾數必須是1.M的形式 ,規格化 => 1.705 2

4.舍入處理

浮點運算在對階或右規時,尾數需要右移,被右移出去的位會被丟掉,從而造成運算結果精度的損失。爲了減少這種精度損失,可以將一定位數的移出位先保留起來,稱爲保護位,在規格化後用於舍入處理。
IEEE754標準列出了四種可選的舍入處理方法,默認使用四捨五入。

5.溢出判斷

與定點數運算不同的是,浮點數的溢出是以其運算結果的階碼的值是否產生溢出來判斷的。
若階碼的值超過了階碼所能表示的最大正數,則爲上溢,進一步,若此時浮點數爲正數,則爲正上溢,記爲+∞,若浮點數爲負數,則爲負上溢,記爲-∞;若階碼的值超過了階碼所能表示的最小負數,則爲下溢,進一步,若此時浮點數爲正數,則爲正下溢,若浮點數爲負數,則爲負下溢。正下溢和負下溢都作爲0處理

0.1 + 0.2 != 0.3

換算成 IEEE 754 標準的二進制數據結構

在如上基礎之類,我們再開始我們的議題0.1 + 0.2 != 0.3, 計算機首先會把十進制轉爲二進制,然後進行加法。
0.1 轉 二進制爲, 終止條件是 直到積中的小數部分爲零, 但是從下面的結果來看, 所以簡單表示爲

0.0 0011 0011 (0011)無限循環,無限循環,這可不行,這次輪到 IEEE 754標準 出場了,IEEE 754標準定義了單精度和雙精度浮點格式

2 * 0.1   
2 * 0.2        0

2 * 0.4        0
2 * 0.8        0
2 * 1.6        1
2 * 1.2        1

2 * 0.4        0
2 * 0.8        0
2 * 1.6        1
2 * 1.2        1

2 * 0.4        0
2 * 0.8        0
2 * 1.6        1
2 * 1.2        1
.....無限循環0011....

我們以0.1爲例,開始計算

  • 符號位S
    因爲是正數,那麼符號位爲 0 。
  • 指數部分E
    首先我們將0.1轉爲2進制數0.0 0011 0011 (0011)無限循環,因爲是正數,那麼符號位爲 0。 然後我們根據正規化的二進制浮點數表達,那麼它以1.bbbbb...這種形式表示, 爲:1.1001 1001 (1001)無限循環 x 2-4
    E-1023 = -4 那麼 E = 1019, 1019的二進制表示爲 1111111011, 因爲有11位,前面加0, 爲01111111011
  • 尾數部分M, 無限循環
    1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001無限循環
    當64bit的存儲空間無法存儲完整的無限循環小數,而IEEE 754 Floating-point採用round to nearest, tie to even的舍入模式。
    1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 就進位爲
    1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

最終
0-01111111011-1001100110011001100110011001100110011001100110011010
你也可使用 IEEE 754 64位轉換工具來驗證自己的結果是否正確。

同理0.2的最終結果爲
0-01111111100-1001100110011001100110011001100110011001100110011010

接下來,進行標準的浮點數運算

對階

小階對大階。
0.1 的指數 01111111011 = 1019
0.2 的指數 01111111100 = 1020
0.2 的指數大,0.1的調整指數位爲 01111111100, 同時位數部分右移一位,如下:

0.1    0-01111111100-11001100110011001100110011001100110011001100110011010
0.2    0-01111111100-1001100110011001100110011001100110011001100110011010   

尾數運算

可以看到有進位

    0.11001100110011001100110011001100110011001100110011010    ---0.1尾數  
    1.1001100110011001100110011001100110011001100110011010     ---0.2尾數      
-----------------------------------------------------------------------------
   10.01100110011001100110011001100110011001100110011001110     
    
         結果

結果規格化

需要右移一位, E+1 = 1020 + 1 = 1021 = 1111111101
1.M = 1.001100110011001100110011001100110011001100110011001110

舍入處理

尾數小數部分 0011001100110011001100110011001100110011001100110011 10 長度爲54,
四捨五入 0011001100110011001100110011001100110011001100110100

溢出檢查

指數沒有溢出

結果計算::

E 爲 1021, S爲0
計算值: (-1)^S*(1.M)*2^(E-1023) => (1.0011001100110011001100110011001100110011001100110100)*2^(-2)
=> 0.010011001100110011001100110011001100110011001100110100

通過 在線進制轉換, 結果爲 0.30000000000000004

話外

既然有這個問題,那麼我們怎麼老保證結果的正確性呢。

  • 比如錢,明確的知道最多兩位小數, 那麼不妨 先 *100
  • 與一個非常小的值對比。比如 Number.EPSILON

浮點數的運算步驟
IEEE 754-維基百科
IEEE 754浮點數標準詳解
JS魔法堂:徹底理解0.1 + 0.2 === 0.30000000000000004的背後
IEEE 754 64位轉換工具

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章