浮動小数点数の内部(IEEE 754と誤差)
なぜ0.1が正確に表せないのかを内部構造から理解でき、金額計算や条件比較のバグを根本から防げるようになる。IEEE 754のビット配置と誤差の発生原理を実務目線で解説。
- 1.IEEE 754のdoubleは「符号1+指数11+仮数52ビット」で、値は 符号 × 1.仮数(2進) × 2^(指数−1023) として表す正規化形。
- 2.0.1や0.2は2進では循環小数になり丸めが入る。だから0.1+0.2が0.30000000000000004になり、== での比較は誤差許容(イプシロン)で行う。
- 3.桁落ち(近い値の減算で有効桁が消える)・情報落ち(桁が違う値の加算で小さい方が無視される)が代表的な精度劣化。和の順序やKahan法で緩和する。
浮動小数点数とは何を表しているか
コンピュータは実数を「2進の科学的記数法」で近似します。10進の 6.022e23 と同じ発想で、値を 符号 × 仮数 × 2^指数 の形に分解して有限ビットに詰めるのが IEEE 754 です。仮数(mantissa / significand)が有効数字、指数がスケール(小数点の位置)を担うため、ごく小さい値から巨大な値まで一定の相対精度で扱えます。固定小数点と違い「点が浮く」のが名前の由来です。
ポイントは、表現できるのは有限個の2進分数だけだということ。実数は連続ですが、64ビットに収まる値は離散的に飛び飛びです。表せない実数は最も近い表現値に丸められ、ここで誤差が生まれます。
ビットレイアウト(binary64 / double)
最も使われる倍精度(double, binary64)は64ビットを次のように配置します。
| フィールド | ビット幅 | 役割 |
|---|---|---|
| 符号 sign | 1 | 0で正、1で負 |
| 指数 exponent | 11 | バイアス1023付き。実際の指数 = 格納値 − 1023 |
| 仮数 fraction | 52 | 1.xxx の小数部分。先頭の1は省略(ケチ表現) |
正規化された値は次式で復元されます(先頭の 1. は格納されない「暗黙のビット」)。
値 = (-1)^符号 × 1.仮数(2進52桁) × 2^(指数フィールド − 1023)
指数にバイアスをかけるのは、ビット列を符号なし整数として比較すれば浮動小数点の大小比較がほぼそのまま成立するように揃えるためです。単精度(float, binary32)は指数8・仮数23ビットで、構造は同じです。
仮数52ビット+暗黙の1ビットで約53ビットの有効精度。10進では約15〜17桁です。「doubleは15桁くらいまで信用できる」が現場の感覚値で、これを超える桁は基本的に意味を持ちません。
なぜ0.1が表せないのか
0.1 を2進にすると 0.0001100110011... と循環します。10進で 1/3 = 0.333... が割り切れないのと同じ理屈で、分母が2の冪でない分数は2進有限小数になりません。52ビットで打ち切って丸めるため、格納される値は厳密な0.1よりわずかにずれます。
0.1(10進) → 0.1000000000000000055511151231257827021181583404541015625(実際の格納値)
0.1 + 0.2 → 0.30000000000000004
これはバグではなく、有限ビットで2進近似する以上避けられない仕様です。型そのものの挙動は 変数とデータ型 でも触れています。等値比較は危険で、許容誤差(イプシロン)を使います。
# NG: 丸め誤差で False になりうる
0.1 + 0.2 == 0.3 # → False
# OK: 相対・絶対の許容差で比較
import math
math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9) # → True
丸めモード
表現できない実数は丸めて格納します。IEEE 754の既定は**最近接偶数丸め(round half to even)**で、ちょうど中間の値は仮数の最下位ビットが偶数になる側へ寄せます。常に切り上げる方式だと誤差が一方向に累積するため、偶数丸めで統計的な偏りを抑えるのが狙いです。他に0方向・正/負無限大方向の丸めも規定されます。
最近接偶数丸めでは 2.5 は 2 に、3.5 は 4 に丸められます(ともに偶数へ)。学校で習う「四捨五入」と結果が違う場面があるので、テストの期待値を作るときに注意します。
特殊な値:非正規数・無限大・NaN
指数フィールドの全0と全1は特別な意味を持ち、ここに浮動小数点の表現力の端が詰まっています。
| 指数フィールド | 仮数 | 表すもの |
|---|---|---|
| 全0 | 0 | ±0(符号付きゼロ。+0と−0が別ビット) |
| 全0 | 非0 | 非正規数(subnormal)。暗黙ビットを0扱いし0付近を埋める |
| 全1 | 0 | ±∞(オーバーフローや 1.0/0.0 の結果) |
| 全1 | 非0 | NaN(0.0/0.0 や sqrt(-1) など不定の結果) |
非正規数は、正規化形では表せない0のすぐ近くの値を、先頭の暗黙ビットを1ではなく0として徐々に精度を落としつつ埋める仕組みです(gradual underflow)。これにより a == b でなければ a - b != 0 が保証されます。ただしハードによっては非正規数の演算が極端に遅く、性能上は flush-to-zero で0に潰す設定が使われることもあります。
NaNは「数でない」を表し、NaN == NaN すら false になる唯一無二の値です。比較が常に偽になる性質はソートや探索を壊すため、別扱いが要ります。
NaN === NaN // false(自分自身とも等しくない)
Number.isNaN(x) // NaN 判定はこの専用関数で
1 / 0 // Infinity(例外ではない)
0 / 0 // NaN
IEEE 754はゼロ除算や不正演算で例外を投げず、∞やNaNを返して計算を続けます。NaNは以降の演算に伝播(NaN汚染)するため、入力検証を怠ると最終結果だけが静かにNaNになりがちです。発生源の特定は 例外処理 とは別の、フラグ確認やガード節の発想が必要です。
演算で精度が劣化する原理
丸め誤差は個々の値だけでなく、演算の組み合わせで増幅します。代表的な2つを区別して理解するのが実務の要点です。
| 現象 | 起きる状況 | 結果 |
|---|---|---|
| 桁落ち | ほぼ等しい2値の減算 | 有効桁が大量に消え、相対誤差が爆発 |
| 情報落ち | 桁数が大きく違う値の加算 | 小さい方が丸めで無視され消える |
情報落ちは、指数を揃える際に小さい値の仮数が右シフトされ、52ビットの外へ追い出されて消える現象です。1e16 + 1.0 が 1e16 に戻るのが典型です。桁落ちは近接値の差で先頭の一致桁が打ち消し合い、残った下位桁の誤差が支配的になる現象です。
# 情報落ち:大きい値に小さい値を足すと消える
1e16 + 1.0 == 1e16 # → True(1.0 が吸収される)
# 桁落ち:近い値の差で有効桁が失われる
a = 1.0000000001
b = 1.0000000000
a - b # 期待 1e-10 だが下位の丸め誤差が露出
対策は、和を絶対値の小さい順に足す、情報落ちを補正する Kahan の総和法(補償項を持ち越す)を使う、桁落ちが出る式を数学的に変形して減算を避ける、などです。(1 - cos x) を 2 sin²(x/2) に直すといった式変形が定石です。
実務での指針
金額は10進で割り切れる前提が崩れると会計が合いません。最小単位の整数(円・セント) で持つか、十進固定小数点(Java BigDecimal、Python decimal、SQL の DECIMAL/NUMERIC)を使います。double の累積誤差は集計件数に比例して効いてきます。
- 比較は
==ではなく許容誤差(絶対・相対の両方)で行う。スケールが大きく変わるなら相対誤差が必須。 - 桁落ちが疑われる式は、減算が先頭に来ない形へ変形する。アルゴリズムの安定性(条件数)を意識する。
- 大量の和は順序や Kahan 法で精度を守る。並列化で加算順が変わると結果が一致しないことも織り込む。
- 再現性が要るテストでは、丸めモードと使用する型(float/double)を固定し、期待値はビット列か十進文字列で記述する。
まとめ
- IEEE 754は値を
符号 × 1.仮数 × 2^指数の正規化形で表し、doubleは1+11+52ビット、有効精度は約15〜17桁。 - 2進有限小数で表せない値(0.1など)は丸められる。等値比較は許容誤差で行うのが原則。
- 指数の全0/全1領域が ±0・非正規数・±∞・NaN を担い、NaNは自身とも等しくならず伝播する。
- 桁落ち・情報落ちが精度劣化の二大原因。和の順序・Kahan法・式変形で緩和し、金額には十進型か整数を使う。
プログラミング Article
浮動小数点数の内部(IEEE 754と誤差)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
浮動小数点
比較で見る軸
難易度: advanced / カテゴリ: プログラミング / タグ数: 5
導入後に効く点
0.1や0.2は2進では循環小数になり丸めが入る。だから0.1+0.2が0.30000000000000004になり、== での比較は誤差許容(イプシロン)で行う。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- プログラミング
- タグ数
- 5
判断チェックリスト
- 自社の用途が「浮動小数点 / IEEE 754」に近いか確認する。
- 強みである「IEEE 754のdoubleは「符号1+指数11+仮数52ビット」で、値は 符号 × 1.仮数(2進) × 2^(指数−1023) として表す正規化形。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。