為甚麼 0.1 + 0.2 不等於 0.3?逐位元理解 IEEE 754 浮點數
DDEVELOPER
開發發布: 11 分鐘閱讀

為甚麼 0.1 + 0.2 不等於 0.3?逐位元理解 IEEE 754 浮點數

在 JavaScript 控制台輸入 0.1 + 0.2,得到的是 0.30000000000000004,而不是 0.3。使用 IEEE 754 binary64 的常見 Python 與 Java 計算也會得到相同的儲存值。這不是程式語言的錯誤,而是十進制小數轉成有限長度的二進制浮點值並經過捨入後的結果。

日文原文發布: 2026-05-30

在 binary64 環境重現結果

把相同輸入捨入為 IEEE 754 binary64,並進行相同加法的實現,會得到相同的儲存結果。不過,各語言與格式化規則顯示的小數碼數仍可能不同。

// JavaScript(Node.js)
> 0.1 + 0.2
0.30000000000000004
> 0.1 + 0.2 === 0.3
false

# Python 3
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

// Java
System.out.println(0.1 + 0.2);
// 0.30000000000000004

// C:double 為 binary64 時
printf("%.20f", 0.1 + 0.2);
// 0.30000000000000004441

ECMAScript 將 Number 定義為 IEEE 754-2019 binary64 值。Python 文件則說明,幾乎所有平台上的 float 都對應 IEEE 754 binary64。C 標準並未要求所有 double 實現都必須是 binary64,因此仍須確認個別實現的特性。

為甚麼有限十進制小數會變成循環二進制小數

一個分數可能在某種進位制中有限終止,換到另一種進位制卻無限循環。十進制的 1/3 會循環;十進制的 0.1 在二進制中也有類似情形:

0.1(十進制)= 0.0001100110011001100110011…(二進制)
0.2(十進制)= 0.0011001100110011001100110…
0.3(十進制)= 0.0100110011001100110011001…

一個最簡分數的二進制展開若且唯若分母是 2 的冪次才會有限終止。因此 1/2、1/4、3/8 可有限表示,但 1/10 的分母仍含因數 5,所以會循環。

十進制分數二進制表示
0.51/20.1(有限)
0.251/40.01(有限)
0.3753/80.011(有限)
0.11/100.000110011…(循環)

IEEE 754 binary64 內部:1 + 11 + 52 位元

一般的 binary64 正規值由 1 個符號位元、11 位元指數欄位與 52 位元儲存小數欄位組成。正規值有效數開頭的 1. 不必明確儲存,因此可得到 53 位元精確度。

┌─┬────────────┬──────────────────────────────────────────────────┐
│S│ 11 位元指數 │                  52 位元小數                      │
└─┴────────────┴──────────────────────────────────────────────────┘

正規值 = (−1)^S × 1.小數 × 2^(指數 − 1023)

指數欄位全為 0 時,小數為 0 代表帶符號零,小數非 0 則代表次正規數。指數欄位全為 1 時,代表無限大或 NaN。

0.1 的 binary64 表示
十六進位:0x3FB999999999999A
位元:    0 01111111011 1001100110011001100110011001100110011001100110011010

確切儲存值:
0.1000000000000000055511151231257827021181583404541015625

循環模式會依可用精確度截斷並捨入,通常採用「捨入至最接近值,遇中間值取偶數」。獨立捨入後的 0.30x3FD3333333333333,而 0.1 + 0.20x3FD3333333333334:兩者相差一個 binary64 間距,約為 5.55 × 10−17

binary16、binary32、binary64 與 binary128

格式總位元數指數碼元儲存小數碼元二進制精確度換算的十進制位數
binary1616510約 3.3 位
binary3232823約 7.2 位
binary64641152約 15.9 位
binary12812815112約 34.0 位

表中的十進制位數由 p × log10(2) 計算,其中 p 包含隱含的前導位元。這不等同於每次十進制轉換都保證能保留的位數,也不等同於十進制往返轉換所需的位數。

不同格式中的 0.1
binary16: 0x2E66              (誤差約 2.44e-5)
binary32: 0x3DCCCCCD          (誤差約 1.49e-9)
binary64: 0x3FB999999999999A  (誤差約 5.55e-18)

浮點差異會在哪些情況造成影響

相等比較

0.1 + 0.2 === 0.3       // false
0.1 + 0.1 + 0.1 === 0.3 // false
0.1 * 10 === 1          // 此運算順序下為 true

對整數或必須具有完全相同位元模式的情況,精確比較是正確做法。近似數值結果則應依計算尺度與用途,採用適當的絕對容許誤差與相對容許誤差。固定使用一次 Number.EPSILON 比較,並不適合所有數值範圍。

金額與十進制規則

金額應以適當的最小貨幣單位整數或十進制類型儲存,並明確規定捨入單位與中間值處理規則。Decimal.from_float(0.1) 顯示的是已捨入 binary64 數值的確切十進制值,並不等於建立精確十進制的 Decimal("0.1")

大型整數

JavaScript Number 只能在 Number.MAX_SAFE_INTEGER(253 − 1)以內區分每一個整數。253 可以表示,但 253 + 1 會與它重疊。超過安全範圍的 ID 與納秒 Unix 時間戳記可考慮使用 BigInt 或字串。目前的微秒 Unix 時間戳記仍低於此上限,因此應評估實際數值範圍,而不能只看單位名稱。

NaN

NaN === NaN          // false
Number.isNaN(NaN)   // true
Number.isNaN("x")   // false

不需要類型強制轉換時,建議使用 Number.isNaN()。全域 isNaN() 會先轉換引數,因此可能把非數值輸入判定成出乎意料的結果。

實用的比較方式

// JavaScript:結合絕對與相對容許誤差
function nearlyEqual(a, b, relTol = 1e-9, absTol = 0) {
  const diff = Math.abs(a - b);
  return diff <= Math.max(absTol, relTol * Math.max(Math.abs(a), Math.abs(b)));
}

# Python
math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)

容許誤差是應用領域的需求,不是通用常數。接近零的數值通常需要非零的絕對容許誤差;大型數值通常需要相對容許誤差。會計、幾何、模擬與測試斷言可能各自需要不同規則。

重點整理

  • 十進制 0.1 的最簡分母不是 2 的冪次,因此在二進制中會循環。
  • binary64 會先捨入 0.1 與 0.2,再進行加法,最後再次捨入結果。
  • 0.30.1 + 0.2 是相鄰的 binary64 值。
  • 精確相等、近似比較、十進制運算與整數運算處理的是不同問題。
  • 請依實際應用領域選擇容許誤差與數值類型。

參考資料及來源

編輯說明

本文使用 AI 協助編輯,並在發布前由編輯核對。內容仍可能包含事實、理解或時效上的錯誤;作出重要決定前,請查閱所列的一手資料或官方文件。

相關文章