0.1 + 0.2 !== 0.3

有一道很常見的面試題面試

0.1 + 0.2 === 0.3 // true ? false
複製代碼

你們應該都知道是 false,可是爲毛不相等呢?下面我將從 浮點數表示浮點數精度 兩個方面來解釋算法

Javascript 浮點數表示

Javascript 中不存在整型和浮點型之分,只有一個類型 Number,它遵循 IEEE 二進制浮點數算術標準(IEEE754),使用 64 位 雙精度浮點數(double) 存儲。bash

雙精度存儲是知道了,那雙精度浮點數是如何存儲數據呢?主要有如下幾個關鍵點spa

1. 雙精度浮點數使用 64 位存儲設計

  • sign(S):符號位,長度爲 1,0 表明數字爲正,1 表明數字爲負
  • exponent(E):指數位,長度爲 11,二進制科學計數法的指數位
  • mantissa(M):尾數位,長度爲 52 ,二進制科學計算法的尾數位

數值的計算公式爲(二進制):code

value = (-1)^S * 2^E * M

2. 使用二進制科學計數法cdn

舉個栗子:
0.5_{10} 轉換爲二進制表示 0.1_2
0.1_2 轉換爲二進制科學計數法 1*2^{-1}blog

3. 指數位表示有符號整數接口

由於指數位數值爲無符號整數,範圍爲[0, 2047],在規格化值中取 [1, 2046]。但指數位須要表示的是有符號整數,則[1, 1022] 表示爲負數[-1022, -1],1023 表示爲 0,[1024, 2046]表示爲正數[1, 1023],因此整個指數位表示的範圍是[-1022, 1023]ip

由上可推導出公式:

E = e - Bias = e - 1023
  • E:指數位表示的數值
  • e:指數位實際存儲的數值

舉個栗子:
0.5_{10}表示爲二進制科學計數法1*2^{-1},該數 E = -1,e = 1022

tips: 下面會講什麼是規格化值,和其餘的值

4. 尾數不表示二進制科學計算的整數部分,即不表示 1

因爲規格化的數整數位都是 1,因此在存儲時能夠節約空間,不表示整數位,從小數位開始表示。因此尾數位其實最大能表示 53 位

由上可推導出公式:

M = 1 + f
  • M:位數位表示的數值
  • f:位數位實際存儲的數值

舉個栗子:
二進制科學計數法1*2^{-1},該數 M=1,則實際存儲 f=0,

由上可將數值計算公式推導爲

value = (-1)^S * 2^{e-1023} * (f + 1)

非規格化值 和 特殊值

那麼問題來了,若是按公式 M = f + 1,那麼如何表示 0 呢?,即便你設 f = 0,e=任意值,按照公式算出,value 不可能爲 0。固然第一個公式 value = (-1)^S * 2^E * M 沒有問題,問題出在公式的推導,由於推導出的公式只符合規格化數值,下面介紹下規格化數值和其餘數值

  • 規格化數值:e 不全爲 0,也不全爲 1,f 爲任意值

  • 非規格化數值:e 所有爲 0,f 爲任意值。非規格化數值主要用於表示 0,以及接近 0 的數。此時公式爲

    E = e = 0,M = f
    value = (-1)^S * f
  • 無窮大:e 所有爲 1,f 爲 0

  • NaN:e 所有爲 1,f 不爲 0

因此:當 e = 0 時,f = 0 時,表示數值 0

解釋 0.1 + 0.2 !== 0.3

到這裏,咱們已經能夠解釋 0.1 + 0.2 爲何不等於 0.3 咯

0.1_{10} 表示爲二進制爲:0.0001100...1100...1100..._2
轉換爲二進制科學計數法爲:1.1001100...1100...1100...*2^{-4}
計算得:S = 0,E = -4,M = 1.1001100...1100...1100...,則 S = 0,e = 1029,f = 1001100...1100...11010
將截斷(舍入)後的數值從新表示爲二進制,則 0.1 最終的二進制數值爲: 0.00011...0011...001101

同理,表示出 0.2,0.3 的二進制數值

0.1:0.0001100110011001100110011001100110011001100110011001101
0.2:0.001100110011001100110011001100110011001100110011001101
0.3:0.010011001100110011001100110011001100110011001100110011

0.1 + 0.2 和爲: // 下面會詳情介紹該步驟
     0.0100110011001100110011001100110011001100110011001101
將和與0.3對比,發現並不相等,中間差值爲:
     0.000000000000000000000000000000000000000000000000000001
複製代碼

從結果值來看,中間差值已經很小很小了,已經能夠忽略了不計了。事實上在 ES6 Number 擴展中,增長 Number.EPSILON 屬性,表示 1 與大於 1 的最小浮點值差,值爲 2^{-52},當值小於 Number.EPSILON 時,通常可忽略不計

浮點數精度

舍入

尾數位只能存儲 52 位,可是在 0~1 之間的實數是無窮盡的,這些無窮的數該如何表示呢?既然徹底表示不可能完成,那麼只有捨棄掉某些數值,來找出最近的浮點數匹配。那麼到底採用哪一種舍入方法呢?下面介紹經常使用的舍入方法

  • 向偶數舍入:也稱爲向最接近值舍入
  • 向零舍入:捨棄末尾位之外的數值,能夠按照 Math.trunc 理解
  • 向正無窮舍入:向較大的數舍入,能夠按照 Math.ceil 理解
  • 向負無窮舍入:向較小的數舍入,能夠按照 Math.floor 理解

IEEE754 採用的是 向偶數舍入,原則是保證損失精度最小,下面簡單介紹一下舍入規則

  1. 對於恰巧中間值的狀況,若是保留位數最後一位是偶數則捨棄後續數值,奇數則進位。例如 1.01_2 保留 1/2 爲 1.0_21.11_2 保留 1/2 爲 1.2_2
  2. 對於向上的值較近,則進位。如 1.01010101 保留 1/2 爲1.1_2
  3. 對於向下的值較近,則捨棄。如 1.001 保留 1/2 爲 1.0_2

舉個栗子:

0.1 => 0.0001100110011001100110011001100110011001100110011001100 | 110011...
// 因爲須要保留的最後一位數後爲 110011...,舍入時離向上的值較近,應該進位,因此
0.1 => 0.0001100110011001100110011001100110011001100110011001101
複製代碼
  • "|" 表示在該處須要舍入

tips:若是你實在沒法判斷如何舍入,有個簡單的辦法。先向下舍入,與原數相減;再向上舍入,與原數相減,將兩個差值比較,取差值絕對值較小的那個數;若是差值相等,則取末尾爲偶數的那個數

計算

爲了進一步保證精度,IEEE754 標準,雙精度在中間計算時,額外保留三位,分別是 保護位、舍入位、粘貼位

  • 保護位:精度最低位右側一位(雙精度能夠理解爲尾數的第 53 位)
  • 舍入位:保護位右側一位
  • 粘貼位:舍入位右側一位,表明舍入位右側是否還有數據,若是右側還有數據,則粘貼位爲 1,不然爲 0,目的是爲了支持目標數值向最近的偶數舍入

在浮點數計算時,經過額外保存三位,來增長計算的正確性,找到浮點數最接近的匹配。

模擬 0.1 + 0.2 計算

講了那麼多,最後仍是簡單寫一下 0.1 + 0.2 的二進制計算過程。

0.1:S = 0,E = -4,M = 1.1001100110011001100110011001100110011001100110011010
0.2:S = 0,E = -3,M = 1.1001100110011001100110011001100110011001100110011010

// 對階 小階對大階
0.1:S = 0,E = -3,M = 0.11001100110011001100110011001100110011001100110011010
0.2:S = 0,E = -3,M = 1.1001100110011001100110011001100110011001100110011010

// 將 M 值相加
和爲:10.01100110011001100110011001100110011001100110011001110

計算出的0.3:S = 0,E = -3,M = 10.01100110011001100110011001100110011001100110011001110

// 規格化
計算出的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110011 | 10

// 計算時,右邊多保留兩位,此處保護位 = 1,舍入位 = 0,粘貼位 = 0
// 舍入後
計算出的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110100
浮點數的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110011

// 轉換10進制
計算出的0.3 = 0.30000000000000004
複製代碼
  • "|" 表示應該截斷,後續數值即爲保護位,舍入位,粘貼位

參考

  • 《深刻理解計算機系統》第二章節
  • 《計算機組成與設計 軟件/硬件接口》 第三章節
  • 維基百科 IEEE 754
相關文章
相關標籤/搜索