為什么 0.1 + 0.2 = 0.300000004 ?
JavaScript 作為一門誕生自上個世紀 90 年代的編程語言[^1],從誕生之初就因為詭異的隱式類型轉(zhuǎn)換等原因被黑,很多 JavaScript 的開發(fā)者還會吐槽浮點數(shù)加法的『奇葩』問題 — 為什么 0.1 + 0.2 在 JavaScript 中不等于 0.3,相信很多人都對這個問題的答案有一個大概的認識,但是都沒有深入研究過,這個問題的答案讓 William Kahan 在 1989 年獲得圖靈獎[^2]。
> 0.1 + 0.20.30000000000000004
其實有上述問題的不止 JavaScript 一門編程語言,幾乎所有現(xiàn)代的編程語言都會遇到上述問題,包括 Java、Ruby、Python、Swift 和 Go 等等,你可以在 https://0.30000000000000004.com/ 中找到常見的編程語言在計算上述表達式的結果[^3]。這不是因為它們在計算時出現(xiàn)了錯誤,而是因為浮點數(shù)計算標準的要求。

圖 1 - 常見的浮點數(shù)『錯誤』
從最開始接觸 C 語言編程,作者就接觸到了浮點數(shù)?float,然而在很長一段時間中,作者都將編程中的浮點數(shù)和數(shù)學中的小數(shù)看做同一個東西,不過當我們重新審視它們時,會發(fā)現(xiàn)這兩個概念的不同之處。
編程中的浮點數(shù)的精度往往都是有限的,單精度的浮點數(shù)使用 32 位表示,而雙精度的浮點數(shù)使用 64 位表示; 數(shù)學中的小數(shù)系統(tǒng)可以通過引入無限序列? ...?可以任意的實數(shù)[^4];
在數(shù)學上我們總有辦法通過額外的符號表示更復雜的數(shù)字,但是從工程的角度來看,表示無限精度的數(shù)字是不經(jīng)濟的,我們期望通過更小和更快的系統(tǒng)表示范圍更大和精度更高的實數(shù)。浮點數(shù)系統(tǒng)是在工程上面做的權衡,IEEE 754 就是在 1985 年建立的浮點數(shù)計算標準,它定義了浮點數(shù)的算術格式、交換格式、舍入規(guī)則、操作和異常處理[^5]。討論浮點數(shù)也無法脫離該標準,為了回答今天的問題,我們將從以下的兩個角度觸發(fā):
[^5]: 754-2019 - IEEE Standard for Floating-Point Arithmetic https://standards.ieee.org/content/ieee-standards/en/standard/754-2019.html
二進制無法在有限的長度中精確地表示十進制中 0.1 和 0.2; 單精度浮點數(shù)、雙精度浮點數(shù)的位數(shù)決定了它們能夠表示的精度上限;
二進制與十進制
我們?nèi)粘I钪惺褂玫臄?shù)字基本都是 10 進制的,然而計算機使用二進制的 0 和 1 表示整數(shù)和小數(shù),所有有限的十進制整數(shù)都可以無損的轉(zhuǎn)換成有限長度的二進制數(shù)字,但是要在二進制的計算機中表示十進制的小數(shù)相對就很麻煩了,我們以 0.375 為例介紹它在二進制下的表示[^6]:
[^6]: Decimal to Binary converter https://www.rapidtables.com/convert/number/decimal-to-binary.html
小數(shù)點后面的位數(shù)依次表示十進制中的 0.5、0.25、0.125 和 0.0625 等等,這個表示方法非常好理解,每一位都是前一位的一半。0.375 在二進制表示看來確實是『整數(shù)』。然而如下圖所示,想要使用二進制表示十進制中的 0.1 和 0.2 是比較復雜的:

圖 2 - 二進制表示的十進制小數(shù)
無論是 0.1 還是 0.2,這兩個數(shù)字都不是二進制中的『整數(shù)』,我們沒有辦法精確地表示它們,只能通過無限循環(huán)小數(shù)嘗試接近它們的真實值;與之相似的是,它們相加的結果 0.3 也無法用有限長度的二進制表示:

圖 3 - 二進制表示的 0.3
這三個不同的數(shù)字都會在最后的小數(shù)部分無限循環(huán) 1100 來趨近于真實值,如果計算機中的浮點數(shù)可以表示無限循環(huán)小數(shù)就有可能解決這個問題,但是事實的真相是浮點數(shù)只會表示有限小數(shù),所有超過特定精度的數(shù)字都會做舍入處理。
精度上限
編程語言中的浮點數(shù)一般都是 32 位的單精度浮點數(shù)?float?和 64 位的雙精度浮點數(shù)?double,部分語言會使用?float32?或者?float64?區(qū)分這兩種不同精度的浮點數(shù)。想要使用有限的位數(shù)表示全部的實數(shù)是不可能的,不用說無限長度的小數(shù)和無理數(shù),因為長度的限制,有限小數(shù)在浮點數(shù)中都無法精確的表示。

圖 4 - 單精度與雙精度浮點數(shù)
單精度浮點數(shù)? float?總共包含 32 位,其中 1 位表示符號、8 位表示指數(shù),最后 23 位表示小數(shù);雙精度浮點數(shù)? double?總共包含 64 位,其中 1 位表示符號,11 位表示指數(shù),最后 52 位表示小數(shù);
我們以單精度浮點數(shù) 0.15625 為例,介紹該浮點數(shù)在計算機二進制中的表示方法,如下圖所示,符號位 0 表示該浮點數(shù)為正數(shù),中間的 8 位指數(shù)總共可以表示 256 個數(shù)字,其中從?[0, 126]?表示?[-127, -1],而?[127, 255]?表示?[0, 128],二進制的 01111100 是十進制的 124,表示?,最后的 23 位是二進制的小數(shù) 0.25:

圖 5 - 0.15625 的單精度浮點數(shù)表示
通過上圖中的公式??可以將浮點數(shù)的二進制表示轉(zhuǎn)換成十進制的小數(shù)。0.15625 雖然還可以用單精度的浮點數(shù)精確表示,但是 0.1 和 0.2 只能使用浮點數(shù)表示近似的值:

圖 6 - 0.1 和 0.2 的單精度浮點數(shù)表示
因為 0.2 和 0.1 只是指數(shù)稍有不同,所以上圖中只展示了 0.1 對應的單精度浮點數(shù),從上圖的結果我們可以看出,0.1 和 0.2 在浮點數(shù)中只能用近似值來代替,精度十分有限,因為單精度浮點數(shù)的小數(shù)位為 23,雙精度的小數(shù)位為 52,同時都隱式地包含首位的 1,所以它們的精度在十進制中分別是??和??位。
因為 0.1 和 0.2 使用單精度浮點數(shù)表示的實際值為 0.100000001490116119384765625 和 0.20000000298023223876953125[^7],所以它們在相加后就得到的結果與我們在一開始看到的非常相似:

圖 7 - 0.1 加 0.2 的結果
上圖只是使用單精度浮點數(shù)表示的數(shù)字,如果使用雙精度浮點數(shù),最終結果中的 3 和 4 之間會有更多的 0,但是小數(shù)出現(xiàn)的順序是非常相似的。浮點數(shù)的運算法則相對來說比較復雜,感興趣的讀者可以自行搜索相關的資料,我們在這里不展開介紹了。
總結
當我們在不同編程語言中看到 0.300000004 或者 0.30000000000000004 時不應該感到驚訝,這其實說明編程語言正確實現(xiàn)了 IEEE 754 標準中描述的浮點數(shù)系統(tǒng),在使用單精度和雙精度浮點數(shù)時也應該牢記它們只有 7 位和 15 位的有效位數(shù)。
在交易系統(tǒng)或者科學計算的場景中,如果需要更高的精度小數(shù),可以使用具有 28 個有效位數(shù)的 decimal 或者直接使用分數(shù),不過這些表示方法的開銷也隨著有效位數(shù)的增加而提高,我們應該按照需要選擇最合適的方法。重新回到今天的問題 — 0.1 和 0.2 相加不等于 0.3 的原因包括以下兩個:
使用二進制表達十進制的小數(shù)時,某些數(shù)字無法被有限位的二進制小數(shù)表示; 單精度和雙精度的浮點數(shù)只包括 7 位或者 15 位的有效小數(shù)位,存儲需要無限位表示的小數(shù)時只能存儲近似值;
浮點數(shù)系統(tǒng)的設計是一個比較有趣的工程問題,因為操作系統(tǒng)一般都是 32 位或者 64 位的,浮點數(shù)充分利用了 32/64 位的比特,將每一位的作用都發(fā)揮到極致,使用最緊湊和簡潔的方式實現(xiàn)了盡可能高的精度。到最后,我們還是來看一些比較開放的相關問題,有興趣的讀者可以仔細思考一下下面的問題:
有哪些編程語言內(nèi)置了高精度的浮點數(shù)或者小數(shù)? 如何實現(xiàn)一個可以精確表示所有實數(shù)(包括有理數(shù)和無理數(shù))的系統(tǒng)?
---END---
長按進入小程序,進行30天打卡
(更多精彩值得期待……)
最近熱文: 終于把QQ和微信一鍵打通了,功能實用! 推薦一款牛逼的Windows神器!功能很強大! 程序員必備基礎:Git 命令全方位學習 提高國內(nèi)訪問 GitHub 的速度的 9 種方案 為什么建議大家使用 Linux 開發(fā)?爽! Windows給力!可以扔掉Linux虛擬機了! IDEA 2020.2 最新破解教程,有效期到2089年! LeetCode1-50題匯總,速度收藏!
2T技術資源大放送!包括但不限于:C/C++,Linux,Python,Java,人工智能,考研,軟考,英語,等等。在公眾號內(nèi)回復「資源」,即可免費獲??!回復「抽獎」,可以參與國慶送書活動! 點個在看少個 bug??


