
浮動小数点数 (IEEE 754) 入門 - なぜ 0.1 + 0.2 は 0.3 にならないのか
「0.1 + 0.2 が 0.3 にならない」——プログラミングを始めて電卓代わりに数式を打ち込み、多くの人が一度は面食らう挙動です。バグではなく、コンピュータが小数を扱う仕組みそのものから来る、避けて通れない性質です。
この記事では、なぜ十進の小数が二進で正確に表せないのかという根本から、IEEE 754(コンピュータの浮動小数点演算を定めた国際規格)が小数をどうビットに詰めているか、そして実務でどう付き合えばよいかまでを、Python 公式ドキュメント・MDN・IEEE 754 の解説を一次/準一次ソースに整理します。
まず実演 - 0.1 + 0.2 の結果
多くの言語で、次の式は直感に反する結果を返します。
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
FalseJavaScript の Number、Python の float、Java の double、C の double ——これらはいずれも同じ IEEE 754 倍精度の二進浮動小数点数です。だから言語が違っても、同じ 0.30000000000000004 が出てきます。これは言語のバグではなく、規格どおりの正しい挙動です。
なぜ十進の 0.1 は二進で表せないのか
カギは「十進で有限の小数が、二進では無限に循環することがある」点にあります。
私たちが日常で使う十進では、0.1 は 1/10 できれいに表せます。しかしコンピュータが内部で使う二進(基数2)では、分母が2のべき乗になる小数(0.5 = 1/2、0.25 = 1/4、0.125 = 1/8 など)しか有限桁で表せません。1/10 は分母に5を含むため、二進では割り切れず無限に循環します。
Python 公式ドキュメントによれば、0.1 の二進表現は次のように循環します。
0.0001100110011001100110011001100110011001100110011...これは十進の 1/3 = 0.3333... が永遠に続くのと同じ現象です。基数(底)が違うと「割り切れる数」も変わる、というだけのことです。
有限のビット数しか持てないコンピュータは、この無限循環をどこかで打ち切って一番近い表現可能な値に丸めて格納します。Python 公式ドキュメントは、倍精度で実際に格納される 0.1 の正確な値を次のように示しています。
0.1000000000000000055511151231257827021181583404541015625つまり、コードに 0.1 と書いた時点で、すでにほんのわずかに大きい値が入っています。0.2 も同様にずれており、その2つを足した結果が、0.3 を丸めた値とは一致しないため、0.1 + 0.2 === 0.3 は false になるわけです。
NOTE
画面に 0.1 と表示されるのは、多くの言語が「元の値に戻せる最短の十進表記」を選んで表示しているからです。内部の値そのものは上記のとおり 0.1 ではありません。
IEEE 754 の構成 - 符号・指数部・仮数部
IEEE 754 の浮動小数点数は、十進でいう「科学的記数法」(例: 6.022 × 10^23)の二進版です。1つの数を次の3つの部分に分けて格納します。
- 符号部 (sign): 正か負か(1ビット)
- 指数部 (exponent): 小数点の位置(2の何乗か)
- 仮数部 (significand / mantissa): 有効数字にあたる部分
値はおおよそ「符号 × 仮数 × 2^指数」という形で表されます。
ケチ表現(暗黙の先頭1)
二進で正規化すると、仮数は必ず 1.xxxx... の形(先頭が1)になります。0 だけは例外ですが、それ以外では先頭は常に1だとわかっているので、その1はビットとして格納しません。この省略を「ケチ表現」や「暗黙の先頭ビット(hidden bit)」と呼びます。おかげで仮数を1ビット分多く使え、精度が実質1ビット得します。
指数部のバイアス
指数部は正にも負にもなり得ます(2^3 も 2^-3 もあり得る)。IEEE 754 では負の指数を符号付きで持たず、一定の値(バイアス)を足してすべて0以上の整数として格納します。倍精度のバイアスは 1023、単精度は 127 です。
binary32 と binary64 のビット配分
実務で出会う代表は、単精度 (binary32 / float) と倍精度 (binary64 / double) の2つです。ビット配分をまとめます。
| 形式 | 別名 | 全体 | 符号部 | 指数部 | 仮数部(格納) | 指数バイアス |
|---|---|---|---|---|---|---|
| binary32 | 単精度 / float | 32 bit | 1 bit | 8 bit | 23 bit | 127 |
| binary64 | 倍精度 / double | 64 bit | 1 bit | 11 bit | 52 bit | 1023 |
有効桁数(十進換算のおおよその精度)は次のとおりです。
- 単精度: 仮数は格納23ビット + 暗黙の1ビットで実質24ビット。十進でおよそ 7桁(約7.22桁)
- 倍精度: 仮数は格納52ビット + 暗黙の1ビットで実質53ビット。十進でおよそ 15〜17桁(約15.95桁)
「およそ7桁」「およそ15桁」という感覚は重要です。たとえば単精度で 123456789 のような9桁の整数を扱うと、後ろの桁が正確に保てません。精度が足りないと感じたら、まず倍精度を使うのが基本です。
特殊値 - ゼロ・無限大・NaN・非正規化数
IEEE 754 は通常の数以外に、いくつかの特別な値を定義しています。
正のゼロと負のゼロ
符号ビットがあるため、+0 と -0 の両方が存在します。比較では +0 === -0 は真として扱われますが、1 / +0 は +Infinity、1 / -0 は -Infinity と、割り算では区別されます。
無限大 (Infinity)
オーバーフローや、ゼロ以外の数をゼロで割った結果などが +∞ / -∞ になります。例外で停止せず、無限大という値として演算を続けられるのが特徴です。
NaN (Not a Number)
0/0 や √(-1) のような「数として定義できない演算」の結果が NaN です。NaN には独特の性質があります。NaN は自分自身を含むあらゆる比較が偽(unordered)になります。
console.log(NaN === NaN); // false
console.log(Number.isNaN(NaN)); // true(判定にはこちらを使う)「ある値が NaN かどうか」を x === NaN で調べることはできません。x !== x が真なら NaN、という小技も成り立ちますが、実務では Number.isNaN()(JS)や math.isnan()(Python)を使うのが安全です。
非正規化数 (subnormal / denormal)
通常の正規化数で表せる最小値よりもさらに小さい、ゼロに近い領域を埋めるための表現です。先頭の暗黙の1を持たず、精度を犠牲にする代わりに、アンダーフロー時に一気にゼロへ飛ばず「徐々にゼロへ近づく(gradual underflow)」挙動を可能にします。
丸めと機械イプシロン
無限循環や桁あふれを有限ビットに収めるには丸めが必要です。IEEE 754 の二進形式における既定の丸めモードは「最近接偶数丸め」(round to nearest, ties to even、銀行家の丸め)です。一番近い表現可能な値に丸め、ちょうど中間だったときは末尾が偶数になる方を選びます。これにより、丸め誤差が一方向に偏って積み上がるのを抑えます。
「1と、1より大きい最小の表現可能な数との差」を機械イプシロンと呼びます。倍精度では 2^-52、およそ 2.220446049250313e-16 です。JavaScript では Number.EPSILON で参照できます。これは「倍精度が1付近で区別できる最小の刻み幅」であり、後述する許容誤差比較の基準になります。
実務での対処法
仕組みがわかれば、対処は難しくありません。場面ごとに整理します。
1. 等値比較を避け、許容誤差(イプシロン)で比べる
浮動小数点同士を === で厳密比較するのは避け、差の絶対値が十分小さいかで判定します。
function nearlyEqual(a, b, epsilon = Number.EPSILON) {
return Math.abs(a - b) < epsilon;
}
console.log(nearlyEqual(0.1 + 0.2, 0.3)); // trueWARNING
Number.EPSILON をそのまま許容誤差に使うのは、値が1付近のときだけ妥当です。1000.1 のような大きな値では刻み幅も大きくなるため、扱う数の大きさに応じて許容誤差をスケールさせる必要があります(例: 1000 * Number.EPSILON)。
2. 金額・通貨は浮動小数点で持たない
最も事故が起きやすいのがお金の計算です。0.1 円の誤差でも、合計すれば数字が合わなくなります。対策は2つです。
- 最小単位の整数で持つ: 円なら「円」、ドルなら「セント」など最小単位の整数で計算し、表示時だけ割って小数にする
- 十進数型 (decimal) を使う: Python の
decimal.Decimal、Java のBigDecimalなど、十進をそのまま正確に扱う型を使う
>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
TrueDecimal は文字列から作る点が重要です。Decimal(0.1) のように float から作ると、すでにずれた 0.1 を取り込んでしまいます。
3. 表示は丸める
内部では誤差を含んだまま計算し、ユーザーに見せる直前に必要な桁数へ丸めるのが定石です。toFixed()(JS)や round() / 書式指定(Python)で、表示桁を明示します。
console.log((0.1 + 0.2).toFixed(2)); // "0.30"4. JavaScript の整数は 2^53 の壁に注意
JavaScript の Number は倍精度なので、小数だけでなく大きな整数でも精度が落ちます。安全に正確な整数として扱える上限は Number.MAX_SAFE_INTEGER、すなわち 2^53 - 1 = 9007199254740991 です。MDN によれば、これを超えると整数が正しく区別できなくなります。
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991(= 2^53 - 1)
console.log(9007199254740992 === 9007199254740993); // true(本来は別の数)ID や金額の通し番号など、この上限を超え得る大きな整数を扱うときは、BigInt を使います。
console.log(BigInt(Number.MAX_SAFE_INTEGER) + 2n); // 9007199254740993nAPI から受け取った大きな ID を JSON.parse で Number にしてしまい、末尾が変わる——という事故は実際によくあります。大きな整数を含む JSON は、文字列として受け取る設計にしておくと安全です。
まとめ
要点を整理します。
- 原因: 十進の
0.1は二進で無限循環するため、コンピュータは近い値に丸めて格納する。だから0.1 + 0.2は0.30000000000000004になる - IEEE 754: 符号・指数部・仮数部で数を表す。単精度 (binary32) は32ビットで有効約7桁、倍精度 (binary64) は64ビットで有効約15〜17桁。暗黙の先頭1ビットで精度を1ビット稼ぐ
- 特殊値:
+0/-0、無限大、NaN(自分自身とも等しくない)、非正規化数。NaN の判定には専用関数を使う - 丸め: 既定は最近接偶数丸め。1付近の刻み幅が機械イプシロン(倍精度で約
2.22e-16) - 対処: 等値比較は許容誤差で、金額は整数か十進数型で、表示は丸める。JavaScript の整数は
2^53 - 1を超えたらBigInt
浮動小数点の誤差は「直せるバグ」ではなく「前提として設計に織り込むもの」です。仕組みを一度理解しておけば、原因不明の1円のズレに悩まされることはなくなります。
関連して、コンピュータが文字をどうビットで表すかは 文字コードと UTF-8 / Unicode の基礎 で、日付時刻という別の「ハマりやすい数値」の扱いは タイムゾーンと日時の扱い で扱っています。あわせてどうぞ。


