N2
NanToo
0.1 + 0.2 が 0.3 にならない本当の理由 — IEEE 754 浮動小数点をビットで読む
DDEVELOPER
開発11 分で読める

0.1 + 0.2 が 0.3 にならない本当の理由 — IEEE 754 浮動小数点をビットで読む

JavaScript のコンソールに 0.1 + 0.2 と打ち込んでみてください。 返ってくるのは 0.3 ではなく 0.30000000000000004 です。 Python でも、 Java でも、 C でも同じ結果になります。 これは言語のバグではなく、 ほぼ全ての現代コンピュータが採用する IEEE 754 浮動小数点規格の必然的な振る舞いです。 本記事では、 なぜそうなるのか — 10進で書くと有限な「0.1」が、 2 進では無限循環する ところから始めて、 binary32 / binary64 / binary16 のビット構造を一つずつ分解し、 「金額計算に float を使うな」 と言われる本当の理由を、 ビット可視化ツールで確かめながら整理します。

#IEEE 754#浮動小数点#binary32#binary64#数値計算#JavaScript#Python

再現実験 — どの言語でも同じ結果になる

まずは事実から確認します。 主要言語のインタラクティブモードで 0.1 + 0.2 を評価すると、 どれも同じ値を返します。

// 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 (printf("%.20f", 0.1 + 0.2);)
0.30000000000000004441

言語が違っても結果が ビット単位で完全に一致します。 これは偶然ではなく、 これらの言語の double / Number / float64 型がすべて IEEE 754 binary64 という同じ規格に従っているからです。 ECMAScript 仕様 (6.1.6.1) も「Number 型は IEEE 754-2019 binary64 の倍精度値である」 と明記しています。

つまりこれは「言語選びでは回避できない」 普遍的な性質です。 では、 何が起きているのでしょうか。

原因 — 10進で「有限」な 0.1 は、 2進では「無限循環」する

コンピュータの中で数は基本的に 2 進数で表されます。 10進数で 1/30.3333… と無限に続くように、 ある分数を別の進数で書くと終わらないことがあります。

0.1 を 2 進数で書き下してみると:

0.1 (10進) = 0.0001100110011001100110011001100… (2進、 1100 が無限ループ)
0.2 (10進) = 0.0011001100110011001100110011001…
0.3 (10進) = 0.0100110011001100110011001100110…

10 進で書けば 0.1 はわずか 1 桁の小数ですが、 2 進では 1100 という 4 ビットパターンが永遠に繰り返す無限循環小数になります。 コンピュータの記憶領域は有限なので、 どこかで打ち切る必要があり、 そこで誤差が生じます。

これは「2 のべき乗で表せる分数だけが、 2 進で有限桁になる」 という単純な数学的事実によります。 0.5 (= 1/2) や 0.25 (= 1/4) は 2 進で 0.10.01 と有限桁で書けますが、 0.1 (= 1/10) の分母 10 は 2 のべき乗ではないので無限循環するのです。

10進値 分数 2進表現
0.5 1/2 0.1 (有限)
0.25 1/4 0.01 (有限)
0.375 3/8 0.011 (有限)
0.1 1/10 0.000110011 0011 0011… (無限循環)
0.3 3/10 0.0100110011 0011… (無限循環)

IEEE 754 binary64 のビット構造 — 1 + 11 + 52 = 64 ビット

では、 この「無限に続く 2 進小数」 を有限のビット数にどう詰め込むか。 これを規格化したのが IEEE 754 です。 最新版は 2019 年 7 月 22 日に発行された IEEE Std 754-2019 で、 ECMAScript / Python / Java / C 等の主要言語の浮動小数点はすべてこの規格 (またはほぼ同等の 754-2008) に従っています。

binary64 (倍精度、 C で double、 JS で Number) の構造はこうです:

┌─┬───────────┬──────────────────────────────────────────────────┐
│S│ 指数 (11) │                  仮数 (52)                        │
└─┴───────────┴──────────────────────────────────────────────────┘
 ↑     ↑                              ↑
 符号  バイアス 1023 を引いた値        暗黙の 1. を加えた値

値の復元式は (−1)S × 1.M × 2E−1023 となります (正規化数の場合)。 ポイント:

  • 仮数 (mantissa) は 52 ビットしか持たないが、 先頭の「1.」 を 暗黙で持つので実効 53 ビット精度
  • 指数 (exponent) は 11 ビット、 1023 を引いた値が「真の指数」
  • 指数フィールドがすべて 0 なら 非正規化数 (subnormal)、 すべて 1 なら ∞ または NaN

具体例として、 0.1 を binary64 に詰めると次のようになります:

0.1 の binary64 表現:
  16進:    0x3FB999999999999A
  ビット:  0 01111111011 1001100110011001100110011001100110011001100110011010
           ↑    ↑                                                          ↑
           符号  指数 1019 (バイアス前)                                      末尾は丸めで 1010
                = 1019 − 1023 = −4 (真の指数)

  → 1.6 × 2⁻⁴ = 0.1 + 約 5.55e-18 の誤差

末尾の ...1001 1010 に注目してください。 本来 0.1 の 2 進表現は 1100 がループするはずが、 53 ビット目で打ち切られ、 さらに 最近接偶数への丸め (round-to-nearest-even) によって最後が 1010 になっています。 これが 0.1 を倍精度に入れた瞬間に生まれる 初期誤差です。

そして 0.1 と 0.2 を足すと、 それぞれの初期誤差が合算され、 さらに加算自体の丸め誤差も乗ります。 結果として:

0.3       の binary64 = 0x3FD3333333333333
0.1 + 0.2 の binary64 = 0x3FD3333333333334   ← 末尾 1 ビット違い

差 = 約 5.55 × 10⁻¹⁷

「ほぼ等しい」 が「ビット単位で等しくはない」 ― だから === での比較は false を返すのです。

精度の階梯 — binary16 / binary32 / binary64 / float128

IEEE 754 には主要な精度がいくつか定義されています。 用途に応じて使い分けます。

名前 合計 指数 仮数 10 進精度 主な用途
binary16 (half) 16 bit 5 10 約 3.3 桁 GPU・ML 推論・OpenEXR 画像
binary32 (single, float) 32 bit 8 23 約 7.2 桁 3D グラフィックス・組込み
binary64 (double) 64 bit 11 52 約 15.9 桁 科学計算・JS / Python の標準
binary128 (quad) 128 bit 15 112 約 34.0 桁 天文・暗号・特殊数値計算

binary16 は 1981 年に Hitachi の HD61810 で初めて採用され、 現代では機械学習の推論やゲームエンジン (PlayStation 4 以降の HDR レンダリング、 OpenEXR、 GIMP、 OpenGL、 Vulkan 等) で広く使われています。 表現範囲は ±65,504 まで、 最小正規化数は 2−14 ≒ 6.10×10−5、 バイアスは 15 です。

同じ 0.1 という値も、 精度によってビットパターンが変わります:

0.1 を各精度で:
  binary16: 0x2E66                     (誤差 約 2.44e-5)
  binary32: 0x3DCCCCCD                  (誤差 約 1.49e-9)
  binary64: 0x3FB999999999999A          (誤差 約 5.55e-18)

精度を下げるほど誤差は大きくなりますが、 メモリと計算速度は良くなります。 ML や画像処理で精度を落として動かすのは、 このトレードオフを利用しています。

実害が出るパターン — 「ほぼ等しい」 では困る場面

多くの場合、 浮動小数点の誤差は実用上無視できます (5×10−17 なんて、 物理量の計算ではノイズに完全に埋もれます)。 しかし、 次のような場面では実害が出ます。

1. 等値比較 (===)

// JavaScript
0.1 + 0.2 === 0.3     // → false
0.1 + 0.1 + 0.1 === 0.3 // → false
0.1 * 10 === 1.0      // → true (これは偶然成立!)
0.2 - 0.1 === 0.1     // → true (これも偶然!)

「成立する」 か「しない」 かは、 値を 2 進に変換したときの末尾の丸め方次第で決まり、 入力からは予測できません。 浮動小数点を等値比較してはいけないのは、 この予測不可能性のためです。 代わりに Math.abs(a - b) < εNumber.EPSILON 経由の許容比較を使います。

2. 金額計算

「税込み 110 円を 3 等分」 のような計算で float を使うと、 1 円単位で食い違うことがあります。 Python の公式チュートリアルも「会計・高精度計算には decimal モジュール、 または有理数で扱う fractions モジュールを使え」 と明記しています。 通貨は 整数 (最小単位の銭・cent) で持つのが鉄則です。

# Python: 0.1 を Decimal で見ると真の値が見える
>>> from decimal import Decimal
>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

3. 大きな数 + 小さな数 (吸収誤差)

// JavaScript
1e16 + 1 === 1e16  // → true  (整数 1 が吸収される)
9007199254740992 + 1  // → 9007199254740992 (Number.MAX_SAFE_INTEGER 超え)

53 ビットの仮数では、 桁数が 16 を超えた整数は連続した整数として表現できなくなります。 これが Number.MAX_SAFE_INTEGER = 253 − 1 = 9,007,199,254,740,991 の正体です。 ID や Unix Timestamp (マイクロ秒) など、 巨大整数を扱うときは BigInt や文字列が必要になります。

4. NaN の比較

NaN === NaN  // → false (IEEE 754 仕様)
Number.isNaN(NaN) // → true

「NaN は自分自身とも等しくない」 のは IEEE 754 の仕様です。 NaN 判定には必ず isNaN 系の関数を使います。

対策のまとめ — どう書けば誤らないか

場面 推奨
2 つの浮動小数点を等値比較したい Math.abs(a-b) < Number.EPSILON * Math.max(1, Math.abs(a), Math.abs(b)) 等の許容比較。 Python なら math.isclose(a, b)
金額・通貨 最小単位の整数で持つ (円・銭・cent)。 表示直前に小数化。 Python なら decimal.Decimal
大量の数を合計 Python math.fsum() や Kahan の補正加算で誤差累積を抑える
16 桁を超える整数 (ID・タイムスタンプ) JS なら BigInt、 JSON では文字列化、 DB は BIGINT
表示時に「きれいな」 桁数で見せたい toFixed(2)Intl.NumberFormat で丸めた文字列を作る (内部値は触らない)

計算は浮動小数点で速く、 比較と表示は丁寧に」 ― これが基本姿勢です。

仕組みを目で見ながら理解したい方は IEEE 754 ビット分解ツール をどうぞ。 10進値を入れるだけで、 符号・指数・仮数の各ビットと、 入力 (10進) と復元値の誤差まで一覧できます。 「0.1 + 0.2」 が 0x3FD3333333333334 になるのは、 末尾 1 ビットの丸め方の違い ― それが目で見える状態で確認できます。

参考文献・ソース

記事作成に関する注記

本記事は AI(大規模言語モデル)を編集補助として活用して作成しています。 公開前に編集者が内容を確認していますが、事実誤認・仕様の解釈ミス・最新情報との齟齬が含まれる可能性があります。 重要な判断を行う際は、本文中の一次ソースや公式ドキュメントを必ずご自身でご確認ください。 誤りにお気づきの場合は、お問い合わせフォームよりご連絡いただけると助かります。

🔧 関連ツール

📚 関連記事