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 ドキュメント「浮動小数点演算:その問題と制限」

ブログへ