浮動小数点
ふどうしょうすうてん
小数を仮数と指数で近似表現する方式。0.1+0.2が0.3にならない理由はここにある。
概要
浮動小数点は、コンピュータで小数を表すための標準的な方式です。数を「仮数 × 2の指数乗」という形に分解して表現します。10進数でいえば 123000 を 1.23 × 10⁵ と書く科学記法と同じ発想で、小数点の位置が指数によって「浮動」する(動く)ことが名前の由来です。ほぼすべてのCPUと言語が IEEE 754 という共通規格に従っており、float(32ビット)や double(64ビット)という型名でおなじみです。
重要なのは、これが小数の「正確な表現」ではなく「近似表現」だという点です。限られたビット数に無限に細かい実数を詰め込むことはできないため、表せない値は最も近い表現可能な値に丸められます。有名な「0.1 + 0.2 が 0.3 にならない」現象は、バグでも言語の欠陥でもなく、この方式の必然的な性質です。仕組みを知らないと「テストがたまに落ちる」「合計金額が1円ずれる」といった不可解な症状に悩まされることになります。
なぜ生まれたか
小数を扱う素朴な方法として、小数点の位置を固定する「固定小数点」があります。たとえば「必ず小数点以下2桁」と決めれば金額計算には十分ですが、この方式は表せる数の範囲が狭く、原子の大きさ(10⁻¹⁰m)から銀河の距離(10²¹m)までを同じ形式で扱う科学技術計算には全く足りませんでした。浮動小数点は、精度(仮数)と範囲(指数)を分業させることで、限られたビット数で桁違いに広い範囲をカバーする解決策です。
もうひとつの課題は互換性でした。1970年代まで、浮動小数点の形式はメーカーごとにバラバラで、同じプログラムがマシンによって違う答えを出すのが当たり前でした。丸めの規則も未定義で、数値計算の信頼性は機種依存だったのです。これを統一したのが1985年の IEEE 754 規格で、ビット配置・丸め規則・無限大や NaN(非数)といった特殊値の扱いまでを標準化しました。今日どの言語・どのCPUでも 0.1 + 0.2 が同じ 0.30000000000000004 になるのは、この規格のおかげです。
詳細
IEEE 754 のビット配置 — 符号・指数・仮数
IEEE 754 の浮動小数点数は、2進数のビット列を3つのフィールドに区切って解釈します。先頭1ビットが符号(0なら正、1なら負)、続くビットが指数(小数点をどれだけ動かすか)、残りが仮数(数の中身にあたる桁)です。32ビットの単精度では指数8ビット・仮数23ビット、64ビットの倍精度では指数11ビット・仮数52ビットが割り当てられます。
指数フィールドには実際の指数に127(倍精度では1023)を足した値を格納し、負の指数も符号なしで表せるようにしています。また仮数は「先頭は必ず1」になるよう正規化されるため、その1は格納せず暗黙の桁として扱い、1ビットぶん精度を稼いでいます。指数が全部0や全部1のときは特別扱いで、ゼロ・非正規化数・無限大・NaN を表します。細部を暗記する必要はありませんが、「符号・指数・仮数の3部品でできている」「仮数のビット数が精度の上限」という構造は押さえておきましょう。
この構造は、実際の数値がどうビットに刻まれるかを自分の手で確かめると一気に腑に落ちます。
- 復元式
- (−1)^0 × 1.仮数 × 2^(E−127) = (−1)^0 × 1.600000023841858 × 2^(123−127) = 1.600000023841858 × 2^-4
- 実際に格納された値
- 0.100000001490116119384765625
- 入力値との誤差
- +1.490116e-9
10進数を入力するかプリセットを押して、32ビットにどう格納されるかを観察してみてください。ビットをクリックすると直接反転もできます。
0.1 + 0.2 はなぜ 0.3 にならないか
鍵は「10進数で切りの良い小数が、2進数では切りが良いとは限らない」ことです。10進数でも 1/3 は 0.3333… と無限に循環し、有限の桁では書き切れません。それと同じことが、2進数では 0.1 に起こります。0.1 = 1/10 の分母10は素因数に5を含むため、分母が2の累乗で表せず、2進数では 0.000110011001100… という無限循環小数になるのです。
つまり 0.1 と書いた瞬間、変数に入っているのはすでに「0.1 に最も近い倍精度の値」であって 0.1 そのものではありません。0.2 も同様です。このわずかにずれた2つの近似値を足し、結果をまた表現可能な値に丸めた結果が 0.30000000000000004 — 「0.3 の近似値」とは紙一重で別の値になります。逆に 0.5 や 0.25 のように分母が2の累乗の小数、そして仮数に収まる範囲の整数は、浮動小数点でも正確に表せます。
実務での落とし穴と対処
第一の鉄則は、浮動小数点数を == で比較しないことです。0.1 + 0.2 == 0.3 は偽になります。比較は「差が十分小さいか」(許容誤差イプシロンとの比較)で行い、テストフレームワークの多くもそのための近似比較用の関数を用意しています。また、桁の大きく異なる数の加算では小さい方の下位桁が失われる「情報落ち」、近い値どうしの減算では有効桁が消える「桁落ち」が起こり、計算の順序が精度を左右します。誤差は演算のたびに蓄積するため、ループで小さな値を足し続ける処理は特に注意が必要です。
金額計算では浮動小数点を使わないのが原則です。1円・1セント単位の整数として扱うか、10進数を正確に表せる Decimal 型(Java の BigDecimal、Python の decimal、RDBMS の DECIMAL 型など)を使います。JavaScript は数値型が倍精度浮動小数点しかない(整数専用型が長らく無かった)ため、この問題に特に遭遇しやすい言語です。整数でも仮数52ビットを超える約9千兆以上は正確に表せなくなるため、巨大なIDを JSON の数値として受け渡して末尾の桁が化ける、という事故も定番です(文字列か BigInt で扱うのが対策です)。
精度を落とす選択 — 機械学習と fp16/bf16
逆に、あえて精度を落とす世界もあります。機械学習、とくにニューラルネットワークの学習と推論では、個々の数値の精度よりも「同じメモリと帯域で何倍の数を運べるか」が性能を決めるため、16ビットの半精度(fp16)や、指数部を32ビットと同じ8ビットに保ったまま仮数を削った bf16(bfloat16)が標準的に使われます。bf16 は「精度は粗くても、表せる範囲が広いので学習が発散しにくい」という、指数と仮数の配分を用途に合わせて設計し直した好例です。GPU はこうした低精度演算に特化した回路を持ち、LLM の巨大なモデルを現実的なメモリに収める量子化(8ビットや4ビット化)へと、この「精度と資源のトレードオフ」はさらに続いていきます。