IEEE 754 浮動小数点ビットパターン可視化ツール

10進数の小数をIEEE 754の単精度(32ビット)・倍精度(64ビット)ビットパターンに分解して可視化、またはビットパターンから10進数に逆変換します。符号・指数部・仮数部を色分け表示し、NaN・無限大・非正規化数も判別できます。

古典的な例: なぜ 0.1 + 0.2 は 0.3 にならないのか

JavaScriptで `0.1 + 0.2` を計算すると `0.30000000000000004` になり、`0.3` とは一致しません。これは倍精度浮動小数点数のビットパターンを見ると理由がわかります。

倍精度(64ビット)16進数 倍精度(64ビット)2進数
0.1 0x3FB999999999999A 0011111110111001100110011001100110011001100110011001100110011010
0.2 0x3FC999999999999A 0011111111001001100110011001100110011001100110011001100110011010
0.3 0x3FD3333333333333 0011111111010011001100110011001100110011001100110011001100110011
0.1 + 0.2 0x3FD3333333333334 0011111111010011001100110011001100110011001100110011001100110100

0.1も0.2も0.3も、2進数では無限小数(循環小数)になるため、有限の52ビットの仮数部には収まりきらず丸められます。0.1と0.2の丸め誤差の方向がたまたま一致しないため、両者を足した結果は0.3を正確に表すビットパターンとは1ビットだけ異なる値(0x3FD3333333333334)になります。上の変換ツールで実際にこれらの値を入力し、末尾ビットの違いを確認してみてください。

使い方のヒント

  • 0.1・0.2・0.3を倍精度でそれぞれビット化し、0.1+0.2の結果(0.30000000000000004)と見比べると、末尾1ビットだけ異なることが視覚的に確認できます。
  • 「-0」を10進数欄に入力すると、符号ビットだけが1になったビットパターン(+0とは異なる)が得られます。数値としては0===-0ですが、ビット表現は別物です。
  • 指数部がすべて0のとき、仮数部が0ならゼロ、0でなければ非正規化数(精度を犠牲にしてゼロに極めて近い値を表す特殊な表現)になります。
  • 単精度(32ビット)と倍精度(64ビット)を切り替えると、同じ0.1という値でも丸め誤差の大きさが異なる(単精度の方が誤差が大きい)ことが確認できます。
  • ビットパターンからの逆変換では、`0x`接頭辞付きの16進数か、選択した精度と同じ桁数の2進数(`0b`接頭辞は任意)で入力する必要があります。

よくある質問

0.1・0.2・0.3はいずれも2進数では無限に続く循環小数になり、コンピューターは有限桁の仮数部(倍精度なら52ビット)に収めるため途中で丸めます。0.1と0.2それぞれの丸め誤差の方向がたまたま一致しないため、両者を足した結果は0.3を正確に表すビットパターンとわずかに異なる値(0.30000000000000004)になります。これは特定の言語のバグではなく、IEEE 754準拠のすべての言語で共通して起こる現象です。

単精度(float、32ビット)は符号1ビット・指数部8ビット・仮数部23ビット、倍精度(double、64ビット)は符号1ビット・指数部11ビット・仮数部52ビットで構成されます。仮数部のビット数が多いほど有効桁数(精度)が高く、指数部のビット数が多いほど表現できる値の範囲が広くなります。一般的なプログラミング言語の既定の浮動小数点型は倍精度であることが多いです。

指数部がすべて0のビットパターンのうち、仮数部が0でないものを指します。通常の(正規化された)浮動小数点数は仮数の先頭に暗黙の1が付きますが、非正規化数はこれを省略し、代わりに指数を最小値に固定することで、通常表現できる最小値よりもさらにゼロに近い値を表現できます。ただし有効桁数は犠牲になります。

IEEE 754では符号ビットが1ビット独立して存在するため、指数部・仮数部がすべて0でも符号ビットが0なら「+0」、1なら「-0」という2つの異なるビットパターンが生まれます。数値としての比較では0 === -0となり等しいと判定されますが、1/0が+Infinity、1/-0が-Infinityになるなど、演算結果に違いが表れる場合があります。

どちらも指数部がすべて1のビットパターンで表現されます。仮数部がすべて0なら無限大(符号ビットで+Infinity/-Infinityを区別)、仮数部が0でなければNaN(Not a Number、非数)です。0除算や∞-∞のような数学的に定義できない演算の結果として生成されます。
ツールくん

余談ですが ― IEEE 754という「共通言語」ができるまで

1985年以前、コンピューターメーカー各社は独自の浮動小数点数表現を採用していました。IBM・DEC・Crayなど、同じ「0.1」という値を計算しても、計算機が違えば結果の丸め方や表現できる範囲が微妙に異なり、あるマシンで書いたプログラムを別のマシンに移植すると数値計算の結果がわずかに食い違うという問題が常態化していました。この混乱を解消するため、Intel 8087数値演算コプロセッサの設計にも携わったカリフォルニア大学バークレー校のWilliam Kahanらが中心となり、業界横断の標準規格として1985年に「IEEE 754」が策定されました。

IEEE 754の核心的なアイデアは、有限のビット数(32ビットや64ビット)で無限に存在する実数を近似的に表現するにあたり、符号・指数部・仮数部という3つの区分に役割を分担させたことです。指数部が桁の大きさ(スケール)を担当し、仮数部が有効数字の精度を担当することで、非常に小さい数から非常に大きい数まで、限られたビット数でも柔軟に対応できる仕組みになっています。指数部にバイアス(単精度なら127、倍精度なら1023)を足して格納するのは、符号付き整数用の2の補数のような複雑な符号処理をせず、ビット列同士を単純比較するだけで大小関係が判定できるようにするための工夫です。

2進数で「きれいに」表現できる小数は、分母が2のべき乗になる分数(1/2、1/4、3/8など)に限られます。10進数の0.1は分数で書くと1/10ですが、10は2のべき乗ではないため、2進数では0.0001100110011...と無限に続く循環小数になってしまいます。有限桁の仮数部に収めるには途中で打ち切って丸めるほかなく、これが「0.1を正確にコンピューターで表現できない」根本的な理由です。10進数の0.3を10進数の世界で正確に扱いたい会計システムなどが、浮動小数点数ではなく整数演算や専用の10進数型(BCD、decimal型)を使うのはこのためです。

IEEE 754はその後、2008年・2019年に改訂され、半精度(16ビット)や四倍精度(128ビット)、10進浮動小数点数の規定なども追加されましたが、根幹となる符号・指数部・仮数部の3分割構造と丸めモードの考え方は1985年の初版から変わっていません。ほぼすべてのプログラミング言語のfloat/double型がこの規格に準拠しているため、「なぜ0.1+0.2が0.3にならないのか」という疑問は、言語を問わず共通して発生する現象なのです。