IEEE 754は、コンピュータが実数を「符号・指数・仮数」の固定長ビットで近似するための国際規格です。現行版は2019年7月発行のIEEE 754-2019で、Python・JavaScript・C・Java・主要なCPUとGPUのfloat型やdouble型はこの規格の形式に従っています。本記事では、binary32(単精度)とbinary64(倍精度)のビット配分、0.1+0.2が0.3にならない理由、Python・JavaScript・PostgreSQLで結果が分かれる丸めやNaNの扱いを、実行したコードと出力で示します。
まとめ:IEEE 754の要点と型の選び方
- IEEE 754は浮動小数点数の形式・丸め・演算・例外処理を定める規格。現行版は754-2019で、次の改訂プロジェクト(P754)が2024年6月に承認され進行中です。
- binary64(double・float64)は符号1・指数11・仮数52ビットで、10進で約15〜17桁。binary32(float・float32)は符号1・指数8・仮数23ビットで約6〜9桁です。
- 0.1は2進で循環小数になり、binary64には0.1000000000000000055511…として格納されます。0.1+0.2が0.30000000000000004になるのはこの丸めの結果で、バグではありません。
- 同じIEEE 754でも、丸め関数・ゼロ除算・NaNの比較は言語やDBで挙動が違います。PostgreSQLはNaN同士を等しいとみなし、Pythonの
round(2.5)は2、JavaScriptのMath.round(2.5)は3です。 - 迷ったらfloat64を既定にし、金額や数量はDECIMALか最小単位の整数で持ちます。機械学習のbfloat16やFP8はIEEE 754の形式ではありません。
IEEE 754の定義と規格がカバーする範囲
浮動小数点数とは:固定小数点数との違い
本記事で主に扱う2進浮動小数点数は、有限の実数を「±仮数×2の指数乗」の形で表す数値表現です。6.022×10^23のような科学的記数法を2進にしたもので、指数が小数点の位置を動かすため、同じ64ビットで10の308乗から10のマイナス308乗まで扱えます。代わりに有効桁は一定で、大きな値ほど隣の表現可能な値との間隔が広がります。binary64では1.0の隣が1.0000000000000002、1e16の隣は2.0離れています(Pythonのmath.ulpで実測)。
固定小数点数は、小数点以下の桁数をあらかじめ決めて整数として持つ方式です。「円単位で小数2桁」のように桁が決まっている値は誤差なく足し引きできますが、表せる範囲は狭くなります。DBのDECIMAL型や、金額を1円単位・1セント単位の整数で持つ設計がこれにあたります。
IEEE 754の版と現行版:1985年から2019年までの改訂
| 版 | 発行 | 主な内容 |
|---|---|---|
| IEEE 754-1985 | 1985年 | 2進の基本形式・拡張形式と四則演算・平方根・比較 |
| IEEE 754-2008 | 2008-08-29 | 1985年版を置き換え、10進の浮動小数点も対象に |
| IEEE 754-2019 | 2019-07-22 | 小改訂。推奨演算の追加と記述の明確化 |
| P754(次の改訂) | 策定中 | 2024-06-06にPAR承認・754-2019を置き換える予定 |
2008年版は、10進の浮動小数点を扱っていたIEEE 854-1987を取り込み、2進と10進の両方を対象にしました(Wikipedia「IEEE 754」)。2019年版はIEEE SAで2019年7月22日発行のActive Standardとして掲載されており、作業部会の変更点一覧によると、誤差を別の値として返すaugmentedAddition系の演算、minimum・maximum系の演算、tanPiなどが推奨演算として加わりました。基本形式のビット配分は2008年版から変わっていないため、2008年版を前提に書かれた資料でもビット表現の説明はそのまま通用します。次の改訂はP754としてActive PARの段階で、発行日はまだ決まっていません。
規格が定める4つの領域:形式・丸め・演算・例外
IEEE 754はビット配分だけでなく、次の4つを定めています。
- 形式:binary16/32/64/128と、10進のdecimal32/64/128。無限大・NaN・符号付きゼロ・非正規化数の表し方も含みます。
- 丸め:最近接(偶数への丸めと、ゼロから遠い方への丸め)と、0方向・+∞方向・−∞方向の計5種類。2進の既定は「最近接・偶数への丸め」です。
- 演算:四則演算・平方根・積和(FMA)は、正確な値を丸めた結果を返すことが求められます。
- 例外:無効演算・ゼロ除算・オーバーフロー・アンダーフロー・不正確の5種類。既定では処理を止めずに結果を返し、状態フラグを立てます。返す値は例外ごとに違い、無効演算はNaN、ゼロ除算は±無限大、オーバーフローは最近接丸めなら±無限大、不正確は丸めた値です。
例外の既定動作は、Cで確かめられます。Apple clang 21で1.0/0.0を計算すると結果はinfになり、fetestexcept(FE_DIVBYZERO)が真を返しました。0.0/0.0はnanで、無効演算のフラグFE_INVALIDが立ちます。
binary32・binary64のビット配分と値の求め方
形式ごとのビット数・指数バイアス・有効桁の一覧
| 形式 | 主な型名 | 符号/指数/仮数 | バイアス | 最大値 | 10進の有効桁 |
|---|---|---|---|---|---|
| binary16 | half、FP16 | 1/5/10 | 15 | 65504 | 3〜5桁 |
| binary32 | float、float32 | 1/8/23 | 127 | 約3.40×10^38 | 6〜9桁 |
| binary64 | double、float64 | 1/11/52 | 1023 | 約1.80×10^308 | 15〜17桁 |
| bfloat16(IEEE外) | BF16 | 1/8/7 | 127 | 約3.39×10^38 | 2〜4桁 |
最大値と桁数は、numpy 2.0.2のnp.finfoとCのfloat.hで確認した値です。「有効桁」の左の数字は10進の何桁までなら丸めても元の10進表記に戻るか(CのDBL_DIG=15)、右の数字は2進の値を10進で書き出して元のビット列へ確実に戻すのに要る桁数(DBL_DECIMAL_DIG=17)です。ログやJSONにfloat64を書き出すときは17桁、float32は9桁あれば値が変わりません。
仮数部のビット数が1つ少なく見えるのは「ケチ表現」のためです。正規化した2進数は先頭が必ず1になるので、その1を格納せずに省きます。binary64は52ビットを格納して53ビット分の精度を持ちます。
-118.625をbinary32に変換する手順
- 符号:負の数なので符号ビットは1。
- 2進に直す:118は1110110、0.625は0.101なので、118.625は1110110.101。
- 正規化:1.110110101×2^6。指数は6。
- 指数部:6にバイアス127を足した133を8ビットで書いて10000101。
- 仮数部:先頭の1を省いた110110101の後ろを0で埋め、23ビットの11011010100000000000000。
並べると1 10000101 11011010100000000000000、16進で0xC2ED4000です。格納値から元の値に戻すときは、(−1)^符号×(1+仮数部の値)×2^(指数部−127)を計算します。
PythonとJavaScriptでビット列を確かめるコード
手計算の答え合わせには、値をバイト列に詰めてから整数として読み直す方法が使えます。Pythonは標準ライブラリのstructだけで書けます。
import struct
from decimal import Decimal
def f64_bits(x):
n = int.from_bytes(struct.pack('>d', x), 'big')
s = f'{n:064b}'
return s[0], s[1:12], s[12:], hex(n)
def f32_bits(x):
n = int.from_bytes(struct.pack('>f', x), 'big')
s = f'{n:032b}'
return s[0], s[1:9], s[9:], hex(n)
print(f32_bits(-118.625))
print(f64_bits(0.1))
print((0.1).hex())
print(Decimal(0.1))
Python 3.14.6と3.9.6のどちらでも、次の出力になりました。
('1', '10000101', '11011010100000000000000', '0xc2ed4000')
('0', '01111111011', '1001100110011001100110011001100110011001100110011010', '0x3fb999999999999a')
0x1.999999999999ap-4
0.1000000000000000055511151231257827021181583404541015625
JavaScriptはDataViewで同じことができます。Node.js v26.5.0で3fb999999999999aと、Pythonと同じビット列が出ます。
const dv = new DataView(new ArrayBuffer(8));
dv.setFloat64(0, 0.1);
console.log(dv.getBigUint64(0).toString(16));
console.log(Math.fround(0.1));
2つ目の出力に使うMath.fround(0.1)は、0.1をbinary32に丸めた値0.10000000149011612を返します。float32で保存した値をfloat64で読むと末尾にこの誤差が現れるので、「DBに0.1を入れたのに0.10000000149が返る」場合は、保存型や途中の変換にfloat32が使われていないか確認します。
非正規化数・無限大・NaN・符号付きゼロの表し方
指数部が全ビット0か全ビット1のときは、特殊な値として扱います。
- 指数部が全て1・仮数部が0:±無限大。
1e308 * 10はPythonでもinfになります。 - 指数部が全て1・仮数部が0以外:NaN。
nan == nanは偽で、Pythonのmax(nan, 1.0)はnan、max(1.0, nan)は1.0と、引数の順番で結果が変わります。 - 指数部が全て0・仮数部が0:±0。
-0.0 == 0.0は真ですが、math.atan2(0.0, -0.0)は3.14159…、math.atan2(-0.0, -1.0)は−3.14159…と、符号が結果に効く関数があります。 - 指数部が全て0・仮数部が0以外:非正規化数。最小の正規化数(binary64で約2.2×10^−308)より0に近い値を、精度を落としながら表します。binary64の最小の正の値は2^−1074で、Pythonの短い10進表記では
5e-324です。
0.1+0.2が0.3にならない理由と誤差の種類
0.1を2進で表すと循環小数になる仕組み
10進の0.1を2進に直すと0.0001100110011…と「0011」が無限に続きます。binary64は仮数を53ビットで打ち切って最も近い値に丸めるので、格納される値は上のコードの通り0.1000000000000000055511151231257827021181583404541015625です。0.2も同様にわずかに大きく、0.3はわずかに小さい値で格納されます。0.1+0.2の正確な和を再び53ビットに丸めた結果が0.30000000000000004で、0.3の格納値とは1 ULP(この大きさで隣り合う表現可能な値の間隔)だけ異なるため、==は偽になります。
おもしろいことに、numpyのnp.float32(0.1) + np.float32(0.2) == np.float32(0.3)は真でした。float32では丸めの位置が違い、たまたま同じビット列に落ちるためです。「float32なら誤差がない」わけではなく、誤差の出る組み合わせが別の場所に移るだけです。
丸め誤差・桁落ち・情報落ちの実測例
| 誤差の種類 | 式(binary64) | 結果 | 本来の値 |
|---|---|---|---|
| 丸め誤差 | 0.1 + 0.2 | 0.30000000000000004 | 0.3 |
| 情報落ち | 1e16 + 1 − 1e16 | 0.0 | 1 |
| 桁落ち | 1 − cos(1e-8) | 0.0 | 5.0×10^−17 |
| 桁落ち | x²+1e8x+1=0の小さい解 | −7.45×10^−9 | −1.0×10^−8 |
情報落ちは、大きな値に小さな値を足したとき、小さな値が仮数の桁からはみ出して消える現象です。1e16の隣の表現可能な値は2.0離れているので、1を足しても変わりません。桁落ちは、ほぼ等しい2数の引き算で上位の桁が打ち消し合い、有効桁が減る現象です。入力に含まれる丸め誤差が、結果に対して相対的に大きくなることがあります。2次方程式の解の公式をそのまま書くと、小さい方の解は約25%ずれました。
import math
a, b, c = 1.0, 1e8, 1.0
d = math.sqrt(b * b - 4 * a * c)
print((-b + d) / (2 * a))
print((2 * c) / (-b - d))
1行目は-7.450580596923828e-09、式を変形した2行目は-1e-08です。同じ値の引き算を避ける式に書き換えるのが、桁落ちへの基本の対処です。1 - cos(x)も2 * sin(x / 2) ** 2に変えると5.0000000000000005e-17が得られます。
float32の反復加算による累積誤差の実測
float32の0.1を1,000万個足すと、本来は約100万になります。numpy 2.0.2で測ると、cumsumで先頭から順に足した最終値は1,087,937.0で、約8.8%ずれました。axisを指定しないnp.sumはペアワイズ加算を使うため999,989.44まで縮まり、float64に変換してから足すと1,000,000.0149でした。PostgreSQL 18.6でも、real(float4)の0.1を100万行sumすると100958.34、double precisionで100000.00000133288、numericで100000.0です。
float32のまま集計してはいけません。保存がfloat32でも、合計や平均はfloat64に上げてから計算します。
Python・JavaScript・PostgreSQLで結果が分かれる挙動
IEEE 754は形式と基本演算に加え、丸め・例外処理・NaNの比較も規定しています。ただし言語やDBの関数・演算子がその既定動作をそのまま使うとは限らず、ここで結果が分かれます。移植やデータ連携で数値が合わないときは、まずこの章の差を疑ってください。
Python 3.12におけるsum()の精度改善
import math
s = 0.0
for _ in range(10):
s += 0.1
print(s, sum([0.1] * 10), math.fsum([0.1] * 10))
Python 3.9.6では0.9999999999999999 0.9999999999999999 1.0、Python 3.14.6では0.9999999999999999 1.0 1.0でした。Python公式ドキュメントのsum()には「Changed in version 3.12: Summation of floats switched to an algorithm that gives higher accuracy」とあり、3.12から組み込みのsum()が誤差を補正しながら足すようになりました。sum([1e16, 1.0, -1e16])も3.9では0.0、3.14では1.0です。forループの+=は今も補正されません。Pythonのバージョンをまたいでテストの期待値を固定している場合、3.12への更新で数値テストが落ちることがあります。
round(2.5)の結果:言語とDBで異なる端数処理
| 環境 | 式 | 結果 | 端数0.5の扱い |
|---|---|---|---|
| Python 3.14 | round(2.5) | 2 | 偶数へ |
| JavaScript | Math.round(2.5) | 3 | +∞方向へ |
| JavaScript | Math.round(-2.5) | −2 | +∞方向へ |
| C | round(2.5) | 3 | 0から遠い方へ |
| C | nearbyint(2.5) | 2 | 現在の丸めモード(既定は偶数へ) |
| PostgreSQL 18 | round(2.5::float8) | 2 | 偶数へ(プラットフォーム依存) |
| PostgreSQL 18 | round(2.5::numeric) | 3 | 0から遠い方へ |
小数2桁への丸めでは、さらに格納誤差が重なります。Pythonのround(2.675, 2)は2.67、JavaScriptの(1.005).toFixed(2)は”1.00″でした。2.675の格納値は2.67499999999999982…で、端数が0.5より小さいためです。PostgreSQLが型によって端数処理を変えることはPostgreSQL 18の数値データ型のドキュメントに明記されています。アプリとDBの両方で丸める設計では、同じ型・同じ関数に寄せないと1円ずれます。
ゼロ除算とNaNの比較:IEEE 754の既定と違う挙動
- Python:
1.0 / 0.0はIEEE 754の既定(inf)ではなくZeroDivisionErrorを送出します。math.sqrt(-1)もValueErrorです。numpyは既定どおりinfやnanを返し、RuntimeWarningを出します。 - JavaScript:
1 / 0はInfinity、0 / 0はNaNで、例外は出ません。[NaN].indexOf(NaN)は−1、[NaN].includes(NaN)はtrueと、同じ配列操作でもNaNの扱いが割れます。 - PostgreSQL:
'NaN'::float8 = 'NaN'::float8は真で、NaNは'Infinity'より大きいと判定されました。ドキュメントには、並べ替えと索引のためにNaN同士を等しく、すべての非NaN値より大きく扱うとあります。アプリ側で「NaNは何とも等しくない」前提のコードを書くと、DBの結果と食い違います。
JavaScriptでのJSON整数読み込みと精度の限界
JavaScriptのNumberはbinary64なので、すべての整数を他の整数と区別して正確に表せる範囲の上限はNumber.MAX_SAFE_INTEGER=2^53−1=9007199254740991までです(ECMAScript 2026仕様)。Node.js v26.5.0でJSON.parse('{"id": 9007199254740993}').idを読むと9007199254740992に変わりました。64ビット整数のIDや注文番号をJSONで渡すAPIでは、文字列として送るのが安全です。float32でも同じことが起き、np.float32(16777217)は16777216.0になります(2^24を超える整数は奇数を表せません)。
浮動小数点数の比較と誤差を抑える実装
==ではなく許容誤差で比べる:isclose の既定値と0との比較
import math
print(math.isclose(0.1 + 0.2, 0.3))
print(math.isclose(1e-10, 0.0))
print(math.isclose(1e-10, 0.0, abs_tol=1e-9))
出力はTrue、False、Trueです。math.iscloseの既定は相対許容誤差rel_tol=1e-09・絶対許容誤差abs_tol=0.0で、0との比較では相対誤差が成り立たないため、0以外の値はすべてFalseになります。0付近を比べる処理ではabs_tolを必ず指定します。
JavaScriptでよく見るMath.abs(a - b) < Number.EPSILONは、1未満の値にしか向きません。Number.EPSILONは1と次の表現可能な値の差(約2.22×10^−16)なので、2以上の値では隣の表現可能な値との差がこれより大きくなり、実質===と同じ判定になります。値の大きさに比例した許容誤差を使ってください。
総和の誤差を抑える:fsum・ペアワイズ加算・型の昇格
- Python:正確な総和が要る場面は
math.fsum。3.12以降のsum()も補正付きですが、forループでの累積は補正されません。 - numpy:
np.sumは、axisを指定しない場合には部分的なペアワイズ加算を使います。axisを指定した場合は、加算する軸とメモリ配置によって方式が異なります。cumsumや自前のループは順加算なので、float32の長い系列では先にfloat64へ変換します。 - C・C++:
fma(a, b, c)は積和を1回の丸めで計算します。fma(0.1, 10, -1)は5.55×10^−17を返し、0.1 * 10 - 1の0とは違う値になります。前者が0.1の格納誤差を正しく反映した値です。コンパイラが自動でFMAに置き換えると結果が変わるため、再現性が要るコードでは-ffp-contract=offで揃えます。
用途別の型選定:float64・DECIMAL・整数の使い分け
float64を既定にする条件とfloat32で足りる場面
科学技術計算・統計・座標・センサー値の集計は、float64を既定にします。float64は多くの測定値を保持するのに十分な精度を持ちますが、桁落ちや長い累積計算では、入力の有効桁だけでなく最終結果の誤差を確認します。float32は、画像・点群・機械学習のテンソルなどで、必要な精度を満たし、メモリ使用量や計算性能の利点がある場合に選びます。集計精度が不足する処理では、float64への昇格や加算方式の変更を検討します。SIMDやGPUではfloat32の方が1命令で多くの値を処理でき、AVX2の256ビットレジスタにはfloat32が8個、float64は4個入ります(SIMDとは?AVX2・NEONの仕組みと自動ベクトル化・組み込み関数の書き方)。
金額・数量の保存に使うDECIMALと最小単位の整数
from decimal import Decimal
print(int(19.99 * 100), round(19.99 * 100))
print(Decimal('19.99') * 100)
19.99ドルをセントに直すつもりのint(19.99 * 100)は1998になります。19.99の格納値が19.989999…で、100倍しても1999に届かず、int()が切り捨てるためです。round()を挟めば1999に戻りますが、どこか1か所で忘れれば1セントずれます。金額はDBではDECIMAL(MySQL 8.4では最大65桁、MySQL 8.4リファレンス)、アプリではDecimal(文字列から生成する)か、1円・1セント単位の整数で持ちます。MySQLのFLOATとDOUBLEは公式に「近似値」の型と定義されています。DB側の型設計はMySQLとは?特徴とバージョン選定・採用判断を実装目線で解説【2026年版】も参考にしてください。
ただしPostgreSQLのドキュメントも、numericの計算は浮動小数点型より非常に遅いと書いています。「確定した金額として保存・合算する値はDECIMAL、統計や予測の計算はfloat64」と境界を引きます。
FP16・bfloat16・FP8:機械学習の低精度形式とIEEE 754の関係
機械学習で使う低精度形式のうち、IEEE 754に含まれるのはbinary16(FP16)だけです。FP16は最大値が65504と狭く、numpyでnp.float16(65520)はinfになりました。学習中の値がFP16の表現範囲を超えるとinfやNaNが生じます。一方で損失スケーリングは、FP16で0に落ちてしまう小さな勾配(最小の正規化数は約6.1×10^−5)を表現範囲に持ち上げる手法で、オーバーフロー時にはスケールを下げるなどの制御が必要です。
bfloat16はGoogleが定義した形式で、符号1・指数8・仮数7ビットです。指数がfloat32と同じ8ビットなので範囲はfloat32並みに広く、FP16のようなオーバーフローが起きにくい代わりに、有効桁は10進で2〜4桁しかありません。Cloud TPUのbfloat16は非正規化数に対応せず、変換時に0へ落とすと明記されています。FP8(E4M3・E5M2)も同じくIEEE 754の外の形式で、仕様の違いはFP8とは?FP16・BF16・FP4との違いとE4M3/E5M2の仕組み、対応GPU【2026年版】で扱っています。
こうした機械学習向けの形式を標準化するため、IEEEではP3109(Arithmetic Formats for Machine Learning)が2023年2月にPAR承認され、策定が進んでいます。現時点ではNaNの有無や飽和の仕方がベンダーごとに異なるため、使うライブラリとハードウェアの仕様を個別に確認します。モデルの低精度化の判断は量子化(モデル量子化)とは?仕組み・PTQとQATの違いと実装判断を解説【2026年版】にまとめています。数値精度の設計を含めた機械学習モデルの開発は、一操の機械学習モデル開発でも支援しています。
IEEE 754と浮動小数点数に関するよくある質問
IEEE 754の最新版はいつのものですか?
2019年7月22日発行のIEEE 754-2019です。次の改訂(P754)は2024年6月にPARが承認され、策定中です。
単精度float32と倍精度float64はどちらを使うべきですか?
迷ったらfloat64です。float32は10進で約6〜9桁しかなく、加算を繰り返すと誤差が膨らみます。大量データだけfloat32で持ち、集計はfloat64で行います。
なぜ0.1+0.2は0.3にならないのですか?
0.1・0.2・0.3は2進で循環小数になり、53ビットに丸めて格納されます。和を丸めた0.30000000000000004と0.3の格納値が、1 ULPだけ異なるためです。
浮動小数点数の誤差をなくす方法はありますか?
2進の浮動小数点数である限り、0.1の誤差はなくせません。正確な10進が必要ならDecimal・DECIMAL・最小単位の整数を使います。誤差を小さくするならfloat64への昇格、math.fsum、桁落ちを避ける式変形です。
IEEE 754の「ケチ表現」とは何ですか?
正規化した2進数の先頭の1を格納せずに省く方式です。binary32は23ビットの仮数部で24ビット分の精度を持ちます。