この記事で伝えること
-
console.log(0.1 + 0.2)が0.30000000000000004になる理由が説明できるようになります - コンピュータが小数を「2進数」でどう表現しているかがわかります
- IEEE 754という浮動小数点数の標準規格の考え方を理解できます
- 実務で金額計算に浮動小数点数を使ってはいけない理由と、代わりにどうすればいいかがわかります
プログラミングを始めたばかりの頃、多くの人が一度はこの現象に驚きます。私自身も初めて見たときは「バグでは?」と本気で疑いました。今回はその正体を、2進数の仕組みから丁寧に解き明かしていきます。
基礎知識の解説
そもそも「2進数で小数を表す」とはどういうことか
私たちが普段使う10進数では、小数は次のように「10のマイナス乗」の組み合わせで表現されています。
0.25 = 2 × 10^-1 + 5 × 10^-2
コンピュータの世界では、これと同じことを2進数(0と1)でやります。つまり「2のマイナス乗」の組み合わせです。
0.5 = 1 × 2^-1
0.25 = 1 × 2^-2
0.125 = 1 × 2^-3
ここで重要なポイントがあります。10進数の 0.1 は、2進数では綺麗に表現できないのです。
>>> (0.1).hex()
'0x1.999999999999ap-4'
0.1 を2進数で書こうとすると、0.0001100110011001100110011... のように 1100 が無限に繰り返す循環小数 になってしまいます。ちょうど10進数で 1/3 を書こうとすると 0.3333... と無限に続くのと同じ現象です。
有限のビット数に無限小数を詰め込む
コンピュータのメモリは有限なので、この無限に続く小数をどこかで打ち切らなければなりません。これが丸め誤差の正体です。
一般的な言語(Python、JavaScript、Javaなど)では、小数は IEEE 754 という規格に基づく「倍精度浮動小数点数(double)」として扱われます。これは64ビットを次のように使い分ける方式です。
- 符号部: 正負を表す(0=正、1=負)
- 指数部: 小数点の位置(何乗するか)を表す
- 仮数部: 実際の数値(有効桁)を表す
イメージとしては「理科の授業で習った指数表記」に近いです。
1.100110011...₂ × 2^-4
仮数部が52ビットしかないため、0.1 のような循環小数は途中で切り捨てられ、本来の値よりわずかに大きい、または小さい近似値として記憶されます。
なぜ 0.1 + 0.2 が 0.3 にならないのか
0.1 も 0.2 も、それぞれ実際には「真の値にほんの少しだけ誤差を含んだ近似値」としてメモリに格納されています。
0.1 の実際の値 ≈ 0.1000000000000000055511151231257827021181583404541015625
0.2 の実際の値 ≈ 0.200000000000000011102230246251565404236316680908203125
この2つを足すと、誤差同士が積み重なり、0.3 にぴったり一致しない値になります。
>>> 0.1 + 0.2
0.30000000000000004
これは言語のバグではなく、IEEE 754を採用しているほぼ全ての言語で起きる共通の仕様です。
// JavaScript
console.log(0.1 + 0.2); // 0.30000000000000004
# Python
print(0.1 + 0.2) # 0.30000000000000004
具体例・実際の動作
誤差が問題になる典型例:金額の合計
浮動小数点数の誤差が一番怖いのは「お金の計算」です。
total = 0.0
for _ in range(10):
total += 0.1
print(total) # 0.9999999999999999
print(total == 1.0) # False
10回 0.1 を足しただけで、期待する 1.0 とは一致しなくなります。ECサイトの合計金額計算などでこれが起きると、if total == 1000: のような比較が意図通りに動かず、深刻なバグにつながります。
対処法1: 誤差を許容した比較をする
import math
a = 0.1 + 0.2
print(math.isclose(a, 0.3)) # True
== で厳密比較せず、「十分近ければ等しいとみなす」比較関数を使います。
対処法2: 整数で計算する(金額なら「円」ではなく「銭」単位など)
# 100円と200円を足す例(円ではなく「銭」= 1/100円単位の整数で扱う)
price_a = 10050 # 100.50円
price_b = 20025 # 200.25円
total = price_a + price_b
print(total / 100) # 300.75
整数同士の加減算には誤差が生じないため、金額はできるだけ「最小単位の整数」で扱うのが定石です。
対処法3: 10進数を正確に扱う専用の型を使う
Pythonには decimal モジュールという、10進数を正確に扱うための標準ライブラリがあります。
from decimal import Decimal
a = Decimal("0.1") + Decimal("0.2")
print(a) # 0.3
内部で2進数変換をせず10進数のまま計算するため、0.1 + 0.2 が期待通り 0.3 になります。ただし通常の浮動小数点数より処理が重いため、パフォーマンスが必要な数値計算には向きません。
筆者の考え・所感
個人的には、浮動小数点数の誤差は「コンピュータが嘘をついている」のではなく「有限のリソースで無限を近似する」という、コンピュータサイエンスの本質が凝縮された現象だと思っています。無限に続く情報を有限のビット数で表現しようとすれば、どこかで妥協が必要になる。これはメモリの制約から生まれる、いわば必然の副作用です。
実務では、これまで何度か「なぜかテストの数値比較がたまに失敗する」という相談を受けたことがあります。原因を辿ると大抵は浮動小数点数の誤差か、== による厳密比較でした。この仕組みを知っているかどうかで、デバッグにかかる時間は大きく変わってきます。
また面白いのは、この問題が「JavaScriptのバグ」としてネタにされがちなことです。実際にはJavaScriptに限らず、Python・Java・C・Go・Rustなど、IEEE 754を採用するほぼ全ての言語で同じ現象が起きます。特定の言語を叩く前に、まず「これは言語仕様ではなく数値表現の仕様である」と理解しておくと、技術的な議論がより建設的になると感じています。
まとめ
- コンピュータは小数を2進数(2のマイナス乗の組み合わせ)で表現しており、
0.1のような値は循環小数になり正確に表せない - IEEE 754規格では符号部・指数部・仮数部という有限のビット数に値を近似して格納するため、丸め誤差が発生する
- 金額計算など誤差が許されない場面では、整数化や
Decimal型など「誤差を生まない」手段を選ぶことが重要