なぜ0.1+0.2は0.3にならないのか?IEEE 754の正体と実務の回避策

なぜ0.1+0.2は0.3にならない?IEEE 754の正体と実務の回避策 IT・コンピュータ基礎
0.1+0.2が0.3にならない浮動小数点誤差の図解

「0.1 + 0.2 === 0.3」が false になる。

これはバグではない。浮動小数点数(IEEE 754)という規格の「仕様」から生じる、必然的な現象だ。

正直に言おう。コンピュータは、私たちが思っているほど数字に強くない。

情シス22年のキャリアの中で、この「0.1問題」に起因する本番障害を2度経験している。1件は消費税の累積計算で合計額が数円ズレた決済システム、もう1件は単価×数量の繰り返し処理で請求書の端数が合わなくなった基幹システムだ。どちらも原因を特定するまでに丸1日かかった。仕組みを知っていれば、設計段階で防げた話だった。

この記事でわかること:

  • なぜ0.1+0.2が0.3にならないのか(二進法と丸め誤差の仕組み)
  • なぜ「0.2999…」ではなく「0.30000000000000004」になるのか
  • 金額計算・ループ・比較で絶対にやってはいけないこと
  • 言語別の正しい対処法(JavaScript / Python / Java)

1. 結論:0.1は「正確な0.1」ではない

📌 要点:コンピュータは十進法の0.1を正確に表せない。二進法では無限小数になるため、メモリの限界で近似値に丸められる。

コンピュータは十進法(人間の言葉)ではなく、二進法(0と1の世界)で数値を扱う。ここが最大の落とし穴だ。

0.1 は二進法では「割り切れない無限小数」になる。

十進法の 1/10 は、二進法に変換すると次のように無限ループする。

0.000110011001100110011...(永遠に続く)

コンピュータは無限の桁をメモリに保存できない。そのため、ある時点で強制的に「丸め(近似)」る。つまり、0.1はメモリの中では「0.1に限りなく近い、別の何か」として保存されているのだ。

1/3 を十進法で表すと 0.333… と永遠に終わらないのと同じ原理。コンピュータにとっての0.1は、まさにその状態だ。


2. なぜ「0.30000000000000004」なのか?

📌 要点:0.1と0.2の近似値はどちらもわずかに「大きい方向」に丸められており、その誤差が足し算で加算されて末尾に4が現れる。

「なぜ 0.2999… ではないのか?」という疑問はもっともだ。

答えは、保存時の「丸め方向」にある。

倍精度浮動小数点数(IEEE 754)の規格において、0.1 も 0.2 も近似値が本来の値よりわずかに大きい方向に丸められている。その「わずかに大きい近似値」同士を足し合わせると、合計もわずかに大きくなる。それが表示桁数の限界で「0.30000000000000004」として現れるわけだ。


3. コンピュータの「視力」:IEEE 754の正体

📌 要点:IEEE 754は数値を64bitの箱に収める国際規格。有効桁数は約15〜16桁が上限で、それ以降は「見えていない」。

IEEE 754倍精度浮動小数点数の64bit構造図

ほぼすべてのプログラミング言語が採用している規格が IEEE 754(倍精度浮動小数点数) だ。数値を64bitの箱に分けて保存するルールで、構造は以下の通り。

領域bit数役割
符号部1bitプラスかマイナスか
指数部11bit小数点の位置(桁数)
仮数部  52bit  数値の有効桁数(ここが精度の限界)   

仮数部が52bit(隠れた1を含め53bit)しかないことが肝心で、これを十進法に換算すると「約15〜16桁」の精度だ。

コンピュータの「視力」は16桁まで。それより先の細かい数値は見えていない(切り捨てられている)と覚えておけばいい。


4. 実務で起きる「3つの致命的な地雷」

📌 要点:金額の累積計算・ループの終了条件・等値比較の3箇所が特に危険。情シスの現場では金融・EC系で実被害が出る。

① 金額の不整合(金融・EC系)

消費税計算やポイント計算を数万件ループさせると、合計で数円のズレが生じる。

冒頭で触れた私の経験がまさにこれだ。基幹システムの請求書で「なぜか1〜2円合わない」という問い合わせが月に数件来ていた。原因を突き止めるまで1日かかり、修正対応に3日かかった。設計段階で整数演算に統一していれば、起きなかったトラブルだ。

1円のズレが信頼を破壊する決済システムでは致命傷になる。

② 無限ループの罠

// 誤差により 1.0 を正確に踏まないため、止まらない可能性がある
let x = 0;
while (x !== 1.0) {
  x += 0.1;
}

x は 0.9999999999999999 → 1.0000000000000002 と 1.0 を飛び越えることがある。結果、ループが永遠に終わらない。

③ 比較判定の失敗

0.1 + 0.2 === 0.3  // false

条件分岐が意図通りに動かなくなる。「正しいはずのロジックが通らない」という最も原因を特定しにくいバグの温床だ。


5. 【決定版】正しい対処法と使い分け

📌 要点:小数の比較・累積計算に標準の浮動小数点数を使うのは禁止。用途に応じて「整数化」「Decimal型」「EPSILON許容」の3択を使い分ける。

IEEE 754倍精度浮動小数点数の64bit構造図
手法推奨シーン実装例
整数で扱う(最強)金額・決済計算100.5円 → 10050(ミリ円単位)で保持
専用型(Decimal)精度優先の業務Python: Decimal, Java: BigDecimal, C#: decimal
誤差の許容(EPSILON)科学計算・判定Math.abs(a - b) < Number.EPSILON

JavaScript での実装例

// NG:浮動小数点のまま比較
0.1 + 0.2 === 0.3  // false

// OK①:整数化して計算(金額系はこれが鉄則)
(10 + 20) / 100  // 0.3(ミリ円単位で計算)

// OK②:EPSILON で許容範囲を設ける
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON  // true

Python での実装例

# NG:通常の float
0.1 + 0.2 == 0.3  # False

# OK:Decimal 型を使う
from decimal import Decimal
Decimal('0.1') + Decimal('0.2') == Decimal('0.3')  # True

金額計算に関しては、私は一貫して「整数化」を推奨している。Decimal型も優秀だが、チームの習熟度やコードの読みやすさを考えると、ミリ円・ミリポイント単位で整数管理する方がトラブルが少ない。現場で10年以上その方針を貫いてきた結論だ。


FAQ:よくある質問

Q. すべての言語でこの問題が起きますか?

IEEE 754を採用しているほぼすべての言語(JavaScript・Python・Java・C#・Swift等)で起きる。逆にDecimal型を標準搭載しているCOBOLや専用の金融計算ライブラリでは起きにくい。

Q. Python の round() 関数を使えば解決しますか?

表示上は解決できるが、計算の内部では誤差は残っている。
round() は「見た目を整える」ための関数であって、計算精度を上げるものではない。金額計算にはDecimal型か整数化を使うこと。

Q. JavaScriptで金額計算するときの鉄則は?

「円単位ではなく銭・ミリ円単位で保持し、整数として計算。
表示するときだけ100や1000で割る」が実務の定石だ。

Q. 0.1 + 0.2 以外にも起きますか?

十進法で割り切れない小数(0.1, 0.2, 0.3, 0.6, 0.7…)は同様の問題が起きる。
0.25や0.5は二進法でも割り切れるため誤差が出ない。

Q. データベースでの金額保存はどうすべきですか?

FLOAT型やDOUBLE型ではなく、DECIMAL型(またはNUMERIC型)を使うこと。
MySQL・PostgreSQL・Oracle いずれも DECIMAL型が利用可能だ。


まとめ

0.1+0.2 が 0.3 にならないのは、コンピュータが間違っているからではない。「有限のbit数で、無限に続く小数を扱おうとしている」という物理的な制約の話だ。

  • 小数を === で比較しない
  • 金額は「整数化」して計算し、表示のときだけ小数点を打つ
  • float / double は「近似値」と割り切って使う
  • 精度が必要な場面ではDecimal型・BigDecimal型に切り替える

仕組みを理解したエンジニアにとって、これは怪現象ではなく「制御可能な仕様」に変わる。設計の段階でこの知識があるかどうかが、本番障害を防げるかどうかの分岐点になる。

関連記事:インターネットはなぜ混まない?パケット交換の仕組みを解説
関連記事:CPUの仕組みを豆電球で理解する
“`

タイトルとURLをコピーしました