
為甚麼 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.5 | 1/2 | 0.1(有限) |
| 0.25 | 1/4 | 0.01(有限) |
| 0.375 | 3/8 | 0.011(有限) |
| 0.1 | 1/10 | 0.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.3 是 0x3FD3333333333333,而 0.1 + 0.2 是 0x3FD3333333333334:兩者相差一個 binary64 間距,約為 5.55 × 10−17。
binary16、binary32、binary64 與 binary128
| 格式 | 總位元數 | 指數碼元 | 儲存小數碼元 | 二進制精確度換算的十進制位數 |
|---|---|---|---|---|
| binary16 | 16 | 5 | 10 | 約 3.3 位 |
| binary32 | 32 | 8 | 23 | 約 7.2 位 |
| binary64 | 64 | 11 | 52 | 約 15.9 位 |
| binary128 | 128 | 15 | 112 | 約 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.3與0.1 + 0.2是相鄰的 binary64 值。- 精確相等、近似比較、十進制運算與整數運算處理的是不同問題。
- 請依實際應用領域選擇容許誤差與數值類型。
參考資料及來源
- IEEE Std 754-2019 — IEEE 浮點數運算標準 ↗
- Python 教學 — 浮點數運算:問題與限制 ↗
- Python 標準函式庫 — decimal.Decimal.from_float ↗
- ECMAScript 語言規範 — Number 類型 ↗
- ECMAScript 語言規範 — Number.MAX_SAFE_INTEGER ↗
- Python 標準函式庫 — math.isclose ↗
- ECMAScript 語言規範 — Number.isNaN ↗
- Goldberg(1991)— What Every Computer Scientist Should Know About Floating-Point Arithmetic ↗
編輯說明
本文使用 AI 協助編輯,並在發布前由編輯核對。內容仍可能包含事實、理解或時效上的錯誤;作出重要決定前,請查閱所列的一手資料或官方文件。

