Why系列:0.1 + 0.2 != 0.3

發表於2020-12-04

為了知道更多一點,打算自己來一個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位轉換工具

相關文章