0.1 + 0.2 が 0.3 にならない理由:浮動小数点数のしくみ
JavaScript・Python・Java で 0.1 + 0.2 が 0.30000000000000004 になる理由を IEEE 754 倍精度形式で説明し、比較・丸め・金額計算での対処法をまとめました。
ブラウザのコンソールで 0.1 + 0.2 を計算すると 0.30000000000000004 が返ってきます。JavaScript だけの問題ではありません。Python、Java、C#、Go でも結果は同じです。計算を間違えているのではなく、これらの言語が数値を同じ方法で保存しているからです。
コンピューターは小数を2進数で保存する
多くの言語では、小数を含む数値を IEEE 754 倍精度(binary64) 形式で保存します。JavaScript の Number、Python の float、Java や C の double はどれも binary64 です。64ビットを3つに分けて使います。
| 部分 | ビット数 | 役割 |
|---|---|---|
| 符号 | 1 | 正か負か |
| 指数 | 11 | 2の何乗を掛けるか |
| 仮数(小数部) | 52 | 有効数字。先頭の1は保存しないため、実質53ビットの精度 |
10進数で 1/3 が 0.3333… と終わらないように、2進数では分母が2のべき乗でない分数が終わりません。0.1 は 1/10 で、10 には 5 が含まれるため、2進数では 0.000110011001100… が無限に続きます。52ビットで打ち切って丸めるしかなく、実際に保存される値は 0.1 ではありません。
| 入力 | 実際に保存される値 |
|---|---|
| 0.1 | 0.1000000000000000055511151231257827021181583404541015625 |
| 0.2 | 0.200000000000000011102230246251565404236316680908203125 |
| 0.3 | 0.299999999999999988897769753748434595763683319091796875 |
0.1 と 0.2 はわずかに大きく、0.3 はわずかに小さく保存されます。足した結果を最も近い binary64 の値に丸めると、0.3 として保存される値ではなく、その1つ上の値になります。
0.1 + 0.2 = 0.3000000000000000444089209850062616169452667236328125
画面に 0.30000000000000004 と表示されるのは、言語が「読み戻したときに同じ値になる最も短い10進数」を選んで表示するからです。0.1 が 0.1 と表示されるのも同じ規則によるもので、保存された値が正確に 0.1 だからではありません。
整数はどこまで正確か
仮数が53ビットなので、2⁵³(9,007,199,254,740,992)までの整数はすべて正確に表せます。JavaScript の Number.MAX_SAFE_INTEGER が 9,007,199,254,740,991 なのはこのためです。これを超えると、表せる整数の間に隙間ができます。
9007199254740992 === 9007199254740993 // true
データベースの64ビット ID などの大きな整数を、JSON で文字列として受け渡す API が多いのもこのためです。JavaScript で大きな整数を計算するなら BigInt を使います。
実務での対処法
1. == で比較しない
浮動小数点の結果は「十分に近いか」で比較します。
import math
math.isclose(0.1 + 0.2, 0.3) # True(既定の相対許容誤差 1e-9)
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
Number.EPSILON(2⁻⁵²、約 2.22×10⁻¹⁶)は 1 付近での間隔です。値が大きくなると間隔も広がるため、大きな数を比べるときは値の大きさに比例した許容誤差(相対誤差)を使うほうが安全です。
2. 丸めは表示するときに一度だけ
Math.round(1.005 * 100) / 100 は 1.01 ではなく 1 になります。1.005 も正確には保存されず 1.00499999… だからです。計算の途中で丸めを繰り返すと誤差がたまります。計算はそのまま進め、表示するときに toFixed() や Intl.NumberFormat で一度だけ桁をそろえます。
3. 金額を浮動小数点で計算しない
金額のように10進数のまま正確である必要がある値は、次のどちらかで扱います。
- 最小単位の整数で保存する。 円なら円単位、ドルならセント単位の整数にすれば、足し算・引き算で誤差は出ません。
- 10進数専用の型を使う。 Python の
decimal.Decimal、Java のBigDecimal、データベースのDECIMAL/NUMERIC型です。
作り方にも注意が必要です。Java の new BigDecimal(0.1) は、すでに誤差を含んだ double をそのまま写すので 0.1000000000000000055511… になります。文字列から new BigDecimal("0.1") と作って初めて正確な 0.1 になります。Python でも Decimal("0.1") のように文字列を渡します。
バグではなく取り決め
浮動小数点は、広い範囲の数を64ビットに収めるために精度を少し譲った形式です。科学計算、グラフィックス、センサー値のようにわずかな誤差が問題にならない場面では、速くて十分に正確です。誤差が問題になる場面(比較・丸め・金額)だけを知って避ければ大丈夫です。
浮動小数点変換ツールに数値を入れると、binary64 と binary32 で保存したときの符号・指数・仮数のビットと、実際に保存される10進数の値を確認できます。0.1、0.3、9007199254740993 を順に入れてみると、この記事の内容をその場で確かめられます。
参考
- IEEE Std 754-2019, IEEE Standard for Floating-Point Arithmetic
- ECMAScript 仕様(ECMA-262):Number 型と
Number::toString - Python ドキュメント「浮動小数点演算:その問題と制限」