為什么 0.1 + 0.2 = 0.300000004
JavaScript 作為一門誕生自上個(gè)世紀(jì) 90 年代的編程語言[^1],從誕生之初就因?yàn)樵幃惖碾[式類型轉(zhuǎn)換等原因被黑,很多 JavaScript 的開發(fā)者還會吐槽浮點(diǎn)數(shù)加法的『奇葩』問題 — 為什么 0.1 + 0.2 在 JavaScript 中不等于 0.3,相信很多人都對這個(gè)問題的答案有一個(gè)大概的認(rèn)識,但是都沒有深入研究過,這個(gè)問題的答案讓 William Kahan 在 1989 年獲得圖靈獎[^2]。
其實(shí)有上述問題的不止 JavaScript 一門編程語言,幾乎所有現(xiàn)代的編程語言都會遇到上述問題,包括 Java、Ruby、Python、Swift 和 Go 等等,你可以在 https://0.30000000000000004.com/ 中找到常見的編程語言在計(jì)算上述表達(dá)式的結(jié)果[^3]。這不是因?yàn)樗鼈冊谟?jì)算時(shí)出現(xiàn)了錯(cuò)誤,而是因?yàn)楦↑c(diǎn)數(shù)計(jì)算標(biāo)準(zhǔn)的要求。> 0.1 + 0.20.30000000000000004
floating-point-math?圖 1 - 常見的浮點(diǎn)數(shù)『錯(cuò)誤』從最開始接觸 C 語言編程,作者就接觸到了浮點(diǎn)數(shù) float,然而在很長一段時(shí)間中,作者都將編程中的浮點(diǎn)數(shù)和數(shù)學(xué)中的小數(shù)看做同一個(gè)東西,不過當(dāng)我們重新審視它們時(shí),會發(fā)現(xiàn)這兩個(gè)概念的不同之處。- 編程中的浮點(diǎn)數(shù)的精度往往都是有限的,單精度的浮點(diǎn)數(shù)使用 32 位表示,而雙精度的浮點(diǎn)數(shù)使用 64 位表示;
- 數(shù)學(xué)中的小數(shù)系統(tǒng)可以通過引入無限序列
...可以任意的實(shí)數(shù)[^4];
- 二進(jìn)制無法在有限的長度中精確地表示十進(jìn)制中 0.1 和 0.2;
- 單精度浮點(diǎn)數(shù)、雙精度浮點(diǎn)數(shù)的位數(shù)決定了它們能夠表示的精度上限;
二進(jìn)制與十進(jìn)制
我們?nèi)粘I钪惺褂玫臄?shù)字基本都是 10 進(jìn)制的,然而計(jì)算機(jī)使用二進(jìn)制的 0 和 1 表示整數(shù)和小數(shù),所有有限的十進(jìn)制整數(shù)都可以無損的轉(zhuǎn)換成有限長度的二進(jìn)制數(shù)字,但是要在二進(jìn)制的計(jì)算機(jī)中表示十進(jìn)制的小數(shù)相對就很麻煩了,我們以 0.375 為例介紹它在二進(jìn)制下的表示[^6]:[^6]: Decimal to Binary converter https://www.rapidtables.com/convert/number/decimal-to-binary.html小數(shù)點(diǎn)后面的位數(shù)依次表示十進(jìn)制中的 0.5、0.25、0.125 和 0.0625 等等,這個(gè)表示方法非常好理解,每一位都是前一位的一半。0.375 在二進(jìn)制表示看來確實(shí)是『整數(shù)』。然而如下圖所示,想要使用二進(jìn)制表示十進(jìn)制中的 0.1 和 0.2 是比較復(fù)雜的:
decimals-binary-representation圖 2 - 二進(jìn)制表示的十進(jìn)制小數(shù)無論是 0.1 還是 0.2,這兩個(gè)數(shù)字都不是二進(jìn)制中的『整數(shù)』,我們沒有辦法精確地表示它們,只能通過無限循環(huán)小數(shù)嘗試接近它們的真實(shí)值;與之相似的是,它們相加的結(jié)果 0.3 也無法用有限長度的二進(jìn)制表示:
dot-three-binary-representation圖 3 - 二進(jìn)制表示的 0.3這三個(gè)不同的數(shù)字都會在最后的小數(shù)部分無限循環(huán) 1100 來趨近于真實(shí)值,如果計(jì)算機(jī)中的浮點(diǎn)數(shù)可以表示無限循環(huán)小數(shù)就有可能解決這個(gè)問題,但是事實(shí)的真相是浮點(diǎn)數(shù)只會表示有限小數(shù),所有超過特定精度的數(shù)字都會做舍入處理。精度上限
編程語言中的浮點(diǎn)數(shù)一般都是 32 位的單精度浮點(diǎn)數(shù)float 和 64 位的雙精度浮點(diǎn)數(shù) double,部分語言會使用 float32 或者 float64 區(qū)分這兩種不同精度的浮點(diǎn)數(shù)。想要使用有限的位數(shù)表示全部的實(shí)數(shù)是不可能的,不用說無限長度的小數(shù)和無理數(shù),因?yàn)殚L度的限制,有限小數(shù)在浮點(diǎn)數(shù)中都無法精確的表示。
float-and-double圖 4 - 單精度與雙精度浮點(diǎn)數(shù)- 單精度浮點(diǎn)數(shù)
float總共包含 32 位,其中 1 位表示符號、8 位表示指數(shù),最后 23 位表示小數(shù); - 雙精度浮點(diǎn)數(shù)
double總共包含 64 位,其中 1 位表示符號,11 位表示指數(shù),最后 52 位表示小數(shù);
[0, 126] 表示 [-127, -1],而 [127, 255] 表示 [0, 128],二進(jìn)制的 01111100 是十進(jìn)制的 124,表示 ,最后的 23 位是二進(jìn)制的小數(shù) 0.25:
floating-number-example圖 5 - 0.15625 的單精度浮點(diǎn)數(shù)表示通過上圖中的公式 可以將浮點(diǎn)數(shù)的二進(jìn)制表示轉(zhuǎn)換成十進(jìn)制的小數(shù)。0.15625 雖然還可以用單精度的浮點(diǎn)數(shù)精確表示,但是 0.1 和 0.2 只能使用浮點(diǎn)數(shù)表示近似的值:
dot-one-dot-two-floating-number圖 6 - 0.1 和 0.2 的單精度浮點(diǎn)數(shù)表示因?yàn)?0.2 和 0.1 只是指數(shù)稍有不同,所以上圖中只展示了 0.1 對應(yīng)的單精度浮點(diǎn)數(shù),從上圖的結(jié)果我們可以看出,0.1 和 0.2 在浮點(diǎn)數(shù)中只能用近似值來代替,精度十分有限,因?yàn)閱尉雀↑c(diǎn)數(shù)的小數(shù)位為 23,雙精度的小數(shù)位為 52,同時(shí)都隱式地包含首位的 1,所以它們的精度在十進(jìn)制中分別是 和 位。因?yàn)?0.1 和 0.2 使用單精度浮點(diǎn)數(shù)表示的實(shí)際值為 0.100000001490116119384765625 和 0.20000000298023223876953125[^7],所以它們在相加后就得到的結(jié)果與我們在一開始看到的非常相似:
dot-three-floating-number圖 7 - 0.1 加 0.2 的結(jié)果上圖只是使用單精度浮點(diǎn)數(shù)表示的數(shù)字,如果使用雙精度浮點(diǎn)數(shù),最終結(jié)果中的 3 和 4 之間會有更多的 0,但是小數(shù)出現(xiàn)的順序是非常相似的。浮點(diǎn)數(shù)的運(yùn)算法則相對來說比較復(fù)雜,感興趣的讀者可以自行搜索相關(guān)的資料,我們在這里不展開介紹了。總結(jié)
當(dāng)我們在不同編程語言中看到 0.300000004 或者 0.30000000000000004 時(shí)不應(yīng)該感到驚訝,這其實(shí)說明編程語言正確實(shí)現(xiàn)了 IEEE 754 標(biāo)準(zhǔn)中描述的浮點(diǎn)數(shù)系統(tǒng),在使用單精度和雙精度浮點(diǎn)數(shù)時(shí)也應(yīng)該牢記它們只有 7 位和 15 位的有效位數(shù)。在交易系統(tǒng)或者科學(xué)計(jì)算的場景中,如果需要更高的精度小數(shù),可以使用具有 28 個(gè)有效位數(shù)的 decimal 或者直接使用分?jǐn)?shù),不過這些表示方法的開銷也隨著有效位數(shù)的增加而提高,我們應(yīng)該按照需要選擇最合適的方法。重新回到今天的問題 — 0.1 和 0.2 相加不等于 0.3 的原因包括以下兩個(gè):- 使用二進(jìn)制表達(dá)十進(jìn)制的小數(shù)時(shí),某些數(shù)字無法被有限位的二進(jìn)制小數(shù)表示;
- 單精度和雙精度的浮點(diǎn)數(shù)只包括 7 位或者 15 位的有效小數(shù)位,存儲需要無限位表示的小數(shù)時(shí)只能存儲近似值;
- 有哪些編程語言內(nèi)置了高精度的浮點(diǎn)數(shù)或者小數(shù)?
- 如何實(shí)現(xiàn)一個(gè)可以精確表示所有實(shí)數(shù)(包括有理數(shù)和無理數(shù))的系統(tǒng)?
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
評論
圖片
表情
