符号・指数・仮数の3パートで小数を表現するIEEE 754規格。非常に大きい数から小さい数まで幅広く扱えます。
私たちは大きな数を書くとき「3億」を「3 x 10の8乗」のように書くことがあります。この書き方を科学記数法と呼びます。
浮動小数点数は、この科学記数法をコンピュータの2進数で実現したものです。すべての数を3つのパーツに分解して格納します。
・符号:プラスかマイナスか
・仮数:有効数字(何桁分の精度を持つか)
・指数:小数点の位置(どれだけ大きい/小さい数か)
具体例で見てみましょう。-6.5 を2進数にすると -110.1 です。
この -110.1 を「小数点の左に1だけ残す」形に変形します。小数点を左に2つずらすイメージです:
・元の形:-110.1(小数点の左に3桁ある)
・小数点を左に2つ移動:-1.101(小数点の左は1だけになった)
・ずらした分を「x 2の2乗」で記録:-1.101 x 22
10進数でも同じことをします。314.0 → 3.14 x 102(小数点を左に2つずらして、10の2乗をかける)。2進数では「10の2乗」の代わりに「2の2乗」になるだけです。この変形を正規化と呼びます。
正規化した結果の3パーツは:
・符号 = マイナス(-)
・仮数 = 1.101(有効数字)
・指数 = 2(小数点を左に2つずらした)
「浮動」小数点という名前は、指数を変えることで小数点の位置を自由に動かせることに由来します。固定小数点では小数点の位置が固定でしたが、浮動小数点は:
・指数を大きくすれば → 巨大な数(1,000,000,000)
・指数を小さくすれば → 極小の数(0.000001)
1つのフォーマットで両方扱えるのが最大の利点です。上のツールでスライダーを動かすと、この3つのパーツがどう変化するか確認できます。
IEEE 754は世界中のコンピュータが共通で使う浮動小数点の規格です。「どのパソコンで計算しても同じ結果になる」ようにするための国際ルールブックです。
単精度(32ビット)の場合、32ビットを以下の3つに分けます。
・符号(1ビット):0なら正の数、1なら負の数
・指数部(8ビット):小数点をどれだけ動かすか(後述のバイアス表現で格納)
・仮数部(23ビット):有効数字の小数点以下を格納
-6.5を例にすると:
・符号ビット = 1(負)
・指数部 = 10000001(実際の指数2にバイアス127を足した129)
・仮数部 = 10100000...0(1.101の「101」部分だけ格納)
上のツールで-6.5を入力すると、32ビットがこの3色に色分けされて表示されます。
倍精度(64ビット)の場合は、符号1ビット + 指数部11ビット + 仮数部52ビットとなります。仮数部が23ビットから52ビットに増えるため、有効桁数が約7桁から約15桁に向上します。JavaScriptやPythonの数値はこの倍精度を使っています。
指数は「2の3乗」のような正の値だけでなく、「2の-3乗」のような負の値も取りたいです。しかし指数部に2の補数を使うと、浮動小数点数の大小比較が面倒になります。そこで「実際の指数に一定の数(バイアス)を足して、常に正の値として格納する」という工夫をしています。
単精度のバイアスは127です。日常に例えると、気温を「実際の気温+127」で記録するようなものです。-3度なら124、+5度なら132と記録し、読み出すときに127を引けば元の気温がわかります。
・実際の指数が -3 → 127 + (-3) = 124 を格納
・実際の指数が +5 → 127 + 5 = 132 を格納
・格納値が 129 → 129 - 127 = 実際の指数は +2
なぜバイアスを使うのか。理由は大小比較を速くするためです。指数部が常に正の整数(負にならない)なので、2つの浮動小数点数のビット列を先頭から順に比べるだけで大小関係がわかります。もし2の補数を使うと先頭ビットが符号になるため特別な処理が必要になり、ハードウェアが複雑になります。バイアスを使えば「ビット列が大きいほど値も大きい」という単純なルールが保てます。倍精度ではバイアスが1023になります。
正規化とは、小数点の左側にちょうど1だけ残す形に変形することです。10進数の科学記数法で「0.0035」を「3.5 x 10の-3乗」と書くのと同じです。2進数では、先頭のビットが必ず1になるように小数点を動かします。例えば110.1(=6.5)は、1.101 x 22に正規化します。
ここで重要な気づきがあります。正規化すると先頭は必ず「1.」になるのです。必ず1なら、わざわざメモリに保存する必要はありません。この「先頭の1を省略してメモリを節約する」仕組みをケチ表現(hidden bit)と呼びます。読み出すときに「先頭に1.をつける」ことで復元します。
これにより、仮数部23ビットに格納するのは「1.101」の小数点以下「101」だけですが、実質的には24ビット分の精度を持つことになります。1ビット得するだけと思うかもしれませんが、有効桁数が1桁増えるのは精度にとって大きな改善です。上のツールで値を入力すると、仮数部に格納される部分(小数点以下)と、復元時に自動で付加される「1.」の関係が視覚的にわかります。
| 指数部 | 仮数部 | 表す値 |
|---|---|---|
| 全て0 | 全て0 | ±0(ゼロ) |
| 全て0 | 0以外 | 非正規化数(denormal) |
| 1〜254 | 任意 | 正規化数(通常の値) |
| 全て1 (255) | 全て0 | ±Infinity(無限大) |
| 全て1 (255) | 0以外 | NaN(非数) |
普通の数だけでなく、「0で割った結果」や「計算できない結果」もビットで表現する必要があります。IEEE 754では、指数部と仮数部の特別な組み合わせで以下の特殊な値を表します。・±0(ゼロ): 指数部と仮数部が全て0。正のゼロと負のゼロがある(通常は区別しない)・±Infinity(無限大): 1.0 / 0.0 の結果。指数部が全て1で仮数部が全て0・NaN(Not a Number、非数): 0.0 / 0.0 や sqrt(-1) の結果。指数部が全て1で仮数部が0以外。NaN == NaN はfalseになるという不思議な性質を持ちます
非正規化数(denormalized)は、正規化数で表せないゼロに近い極小の値を表す仕組みです。指数部が全て0のとき、通常の「1.xxx」ではなく「0.xxx」として解釈します。これにより、正規化数の最小値とゼロの間に「表現できない空白地帯」ができるのを防ぎます。プログラミングではJavaScriptのNumber.MIN_VALUE(約5 x 10-324)がこの非正規化数の最小値です。
| 項目 | 単精度 (float) | 倍精度 (double) |
|---|---|---|
| 合計ビット数 | 32 | 64 |
| 指数部 | 8ビット | 11ビット |
| 仮数部 | 23ビット | 52ビット |
| バイアス | 127 | 1023 |
| 有効桁数(10進) | 約7桁 | 約15〜16桁 |
| 最大値 | 約3.4 x 10^38 | 約1.8 x 10^308 |
単精度(single precision)とは、32ビット(4バイト)で浮動小数点数を表す形式です。プログラミングでは float と書きます。符号1ビット + 指数部8ビット + 仮数部23ビットで構成され、有効桁数は10進で約7桁です。メモリ消費が少なく計算も速いため、GPUでのグラフィック処理やAIの学習で主に使われます。
倍精度(double precision)とは、64ビット(8バイト)で浮動小数点数を表す形式です。プログラミングでは double と書きます。符号1ビット + 指数部11ビット + 仮数部52ビットで構成され、有効桁数は10進で約15桁。「倍」精度という名前は、単精度の約2倍の精度があることに由来します。JavaScriptの数値はすべて倍精度、Pythonの float も内部的には倍精度です。
有効桁数の違いが実務でどう効くか、具体例で見てみましょう。
・単精度(約7桁):10,000,000 と 10,000,001 は区別できるが、10,000,000.5 は 10,000,000 と同じ値になる
・倍精度(約15桁):10,000,000,000,000,000(1京)レベルまで1の位を区別できる
ゲームやAIでは速度重視で単精度、科学計算や金融では精度重視で倍精度を選びます。
10進数で「きりのいい数」でも、2進数では無限に続く循環小数になることがあります。たとえば0.1は2進数では0.0001100110011...と「0011」が永遠に繰り返されます。これは10進数で1/3 = 0.333...が割り切れないのと同じ現象です。
コンピュータは有限のビット数しか持たないので、この無限小数をどこかで打ち切ります。打ち切った結果、0.1は正確には0.1000000000000000055511151231257827021181583404541015625という微妙に違う値として格納されます。この「本来の値」と「実際に格納された値」のズレが丸め誤差です。
有名な例がプログラミングの0.1 + 0.2 = 0.30000000000000004問題です。0.1も0.2もそれぞれ丸め誤差を持った近似値なので、足し合わせると誤差が合算されて0.3ぴったりにはなりません。これはバグではなく、IEEE 754の仕様どおりの正しい動作です。金額計算など正確さが必要な場面では、整数演算(100円→10000として計算)やDecimal型を使って対策します。
固定小数点数(=小数点の位置が最初から固定された表現)と比べると、浮動小数点のメリットがわかります。例えば8ビットを「整数4ビット+小数4ビット」で固定すると、表せる範囲は 0〜15.9375 までです。これでは惑星の質量(約6 × 1024 kg)や電子の質量(約9 × 10-31 kg)のような極端に大きい・小さい数が全く表現できません。
浮動小数点はこの問題を「小数点の位置を指数で動かす」ことで解決します。同じビット数でも、指数を変えるだけで非常に大きな数から非常に小さな数まで扱えます。
・固定小数点:小数点の場所が固定。精度は一定だが範囲が狭い
・浮動小数点:指数で小数点の場所が変わる。広い範囲を扱える代わりに誤差が生じうる
ものさしで例えると、固定小数点は「1mmの目盛りが一定間隔で並んだ30cmのものさし」。浮動小数点は「近い距離は1mm単位、遠い距離は1km単位で切り替えられる地図の縮尺」のようなイメージです。精度よりも表現できる値の幅を優先したのが浮動小数点の設計思想です。
丸め誤差は1回の計算では非常に小さく、ほぼ無視できます。しかし同じ計算を何度も繰り返すと誤差が積み重なり、最終的に無視できない大きさになることがあります。これを誤差の蓄積(累積誤差)と呼びます。
身近なたとえで言うと、コピーのコピーに似ています。1回コピーしても元とほぼ同じ。しかし100回コピーを繰り返すと、細部が崩れて原型が分からなくなります。浮動小数点も同じで、1回の誤差は1億分の1程度でも、計算を1万回繰り返すと1万分の1の誤差になりえます。
具体的な問題が起きやすい場面を2つ紹介します。
・ループで小さい数を足し続ける:0.1 を1000回足しても 100.0 にならず、わずかにズレる
・大きい数と小さい数を足す「桁落ち」:1000000.0 + 0.000001 は、小さい側の精度が失われて 1000000.0 のままになることがある
対策としては、計算の順番を工夫する(小さい数同士から先に足す)、整数演算に置き換える(金額なら「円」ではなく「銭」単位で整数として扱う)、精度の高い型(倍精度)を使う、などがあります。
丸め誤差があるため、浮動小数点数を == で直接比較するのは非常に危険です。0.1 + 0.2 == 0.3は、数学的には当然trueですが、ほとんどのプログラミング言語でfalseになります。これはバグではなく、0.1+0.2が0.30000000000000004になっているためです。
正しい比較方法は「2つの値の差が十分小さければ等しいとみなす」方法です。Math.abs(a - b) < 0.000000001のように、許容できる誤差の範囲(epsilonと呼びます)を決めて比較します。「完全に同じか?」ではなく「十分に近いか?」で判定するということです。
epsilonの値は、単精度なら1e-6程度、倍精度なら1e-9〜1e-15程度が目安です。ただし比較する値自体が1億のような大きい数の場合、絶対値の差ではなく相対誤差(差 / 値の大きさ)で判定する必要があります。