大きな画面での利用をおすすめします — アニメーションと操作がより快適になります。
入力

浮動小数点数(IEEE 754)

浮動小数点形式が保存できるのは離散した値だけだ。入力がその集合にない場合、丸め規則で表現可能な値を選ぶ。語は符号、指数、仮数のフィールドに分かれ、正規数は ±1.f×2ᵉ と読む。IEEE 754 が形式と丸めを定め、本課は既定の最近接・偶数丸めを使う。

表現可能な値は間隔の変わる目盛りに似ている。正規数は指数が 1 上がると間隔が 2 倍になる。非正規数の領域では固定間隔だ。binary16 は 1+5+10 ビット、binary32 は 1+8+23、binary64 は 1+11+52。仮数を広げれば相対精度、指数を広げれば範囲が増えるが、有限幅で表せる目盛りは有限のままである。

仕組み

ゼロでない有限値を符号、1 以上 2 未満の有効数字部、2 のべきに分ける。0.1=1.6×2⁻⁴、4096=1.0×2¹² だ。形式内で正規数にできるかは範囲にも依存する。

符号は一ビットで持つ。浮動小数点加算は指数合わせなどが必要で、単純な整数加算器だけでは処理できないため、符号を分けることに問題はない。

指数フィールドは e+bias。binary16 は bias=15 で、−4 を 11 として保存する。符号なしの指数フィールドを実指数とともに増加させられる。非負で NaN でない符号化なら、生ビットを符号なし整数として比較して順序を保てる。全符号パターンへの一般化はできない。

仮数フィールドは先頭 1. の後だけを保存する。正規数には必ず 1 があるため、10 保存ビットから 11 ビットの精度が得られる。ゼロには先頭 1 がなく、全ゼロ指数をゼロと非正規数に使う。

binary16 の 10 fraction ビットは [1,2) に 1024 個の目盛りを置く。2ᵉ 倍した正規数の指数区間で、隣接間隔の 1 ulp は 2^(e−10)。0.1 付近では 2⁻¹⁴≈0.000061、4096 からの区間では 4 となり、絶対誤差の尺度は大きさとともに変わる。

最近接丸めは近い目盛りを選び、ちょうど中点なら有効数字部が偶数の側を選ぶ。有限範囲内の隣接値へ丸める誤差は間隔の半分以内だ。偶数丸めは中点を常に同じ方向へ送る偏りを避けるが、任意のデータで誤差が必ず相殺する保証ではない。

ステップ実行

既定例は 0.1 を binary16 へ符号化する。

  1. 16 箇所を三つのフィールドに分ける。小数点自体のビットはなく、位置は指数で解釈する。
  2. 正なので符号ビット 15 は 0。
  3. 1.6×2⁻⁴ に分け、e=−4 とする。
  4. 1.6 は二進数で 1.1001100110011…、1001 が繰り返す。1/10 の既約分母には 5 があり、有限二進小数ではない。有限の桁数を増やしても厳密には表せない。
  5. 指数に偏りを加え、−4+15=11、01011 を書く。
  6. fraction の最初の 10 ビット 1001100110 を保存し、後続 011001… は境界の外へ置く。
  7. 捨てる部分は一間隔の約 0.4 で半分未満なので、切り上げない。
  8. 読み戻すと (1+614/1024)×2⁻⁴=0.0999755859375。
  9. [0.0625,0.125) の間隔は 2⁻¹⁴。次の指数区間では約 0.00012 に倍増し、同じ幅の数直線に入る目盛り数は半分になる。
  10. 入力に対する符号付き誤差は約 −2.4×10⁻⁵、−0.4 間隔。半歩内の入力、概ね 0.09995–0.10001 が同じ 16 ビットへ丸められる。厳密な境界は中点の規則で決まる。

上の作業台は垂直線の左を保持、右を破棄するビットとして示す。下行は保存語で、目盛り帯は 0.1 が二つの値の間にあることを示す。三フィールドが確定すれば符号化は完了し、読み戻しはその解釈だ。理想的な導出では最後の丸めだけが情報を失い、他は表現の変換となる。

0.1+0.2 が 0.3 と一致するとは限らない理由

add は既定で binary16 を使う。入力は先にそれぞれ丸められ、0.1 は 0.0999755859375、0.2 は 0.199951171875 になる。指数差は 1 なので小さい側を一列右へずらす。この例の指数合わせは情報を失わない。

有効数字部の加算は 10.0110011001×2⁻³。[1,2) を越えたので一つ右へずらして指数を増やし、1.0011001100|1×2⁻² にする。境界外の一ビットは正確に半間隔。偶数丸めは …1100 を保ち、…1101 へ上げず、0.2998046875 を返す。

一方、0.3 を直接 binary16 にすると 0.300048828125 で、一目盛り上だ。したがって比較は偽。入力の丸めと結果の丸めが決めた差である。

Same sum, binary32 は入力と演算を binary32 で丸め、fraction 23 ビットで比較が真になる。一般的な binary64 では再び偽で、Python の浮動小数点や JavaScript Number の例は 0.30000000000000004 となる。十進表記だけで相等を予測できないが、結果はランダムではない。形式と操作を決めれば確定する。

ほかのプリセットも同じ規則を使う。

  • Absorbed: 4096 + 1:4096 から上の binary16 間隔は 4。1 は四分の一間隔なので結果は 4096 のまま、a+b==a。
  • Overflow: 65504 + 100:有限値の和が binary16 の最大有限値を越え、無限大へ丸められる。IEEE 754 は overflow と inexact の状態を立てる。非トラップ環境では続行でき、言語やライブラリーは報告や例外も選べる。
  • Subnormal: 0.00004:指数フィールドが 0 となり、暗黙の先頭 1 を使わない。固定間隔の非正規数領域に入り、値が小さいほど相対精度が減る。

境界ケース

  • ゼロは指数と fraction が全ゼロで、符号を残すため -0.0 がある。0.0 と等しいがビットは異なる。IEEE の非トラップ除算なら 1/0.0 と 1/-0.0 は逆符号の無限大。Python の float ゼロ除算などは例外を送出する。
  • binary16 の最小正規数は 2⁻¹⁴。その下では有効な指数を固定し、先頭を 0 にして最小正値 2⁻²⁴ まで表す。相対的な有効ビットを減らす漸進的アンダーフローだ。
  • 全一指数とゼロ fraction は無限大、非ゼロ fraction は NaN。有限値を無限大に足すとその無限大になる。inf-inf、inf*0、inf/inf は NaN で、NaN は自身を含むどの値とも等しくない。inf+inf は inf のまま。
  • 丸めが指数を変えることもある。2047.9 を binary16 にすると仮数から桁上がりし、指数を増やして 2048 になる。
  • 0.375=0.011₂ は誤差なく表せる。既約分母が 2 のべきなら有限二進展開だが、有効桁と指数も形式に収まる必要がある。0.5、0.25、2.75 はここでの形式で厳密だ。

よくある間違い

  • 計算結果を無条件に == で比べる。代数的に同じ式でも丸め経路は違う。近似比較が必要なら問題に合わせて abs(a - b) <= max(rel * max(abs(a), abs(b)), abs_tol) の相対・絶対許容差を選ぶ。等値比較自体は禁止ではなく、binary64 で絶対値 2⁵³ 以下の整数や未変更の同一保存値などは厳密に比較できる。NaN は別規則だ。
  • 桁数を増やせば 0.1 が厳密になると思う。binary64 も有限二進形式で、誤差を小さくするか移すだけだ。
  • 加算の結合則を仮定する。(a+b)+c と a+(b+c) は丸め位置が違う。大小の項を足すと小さい項が吸収され得る。順序変更や補償和は誤差を減らせるが、効果はデータによる。
  • 十進で厳密な金額を二進 float へ直接任せる。百分の一は通常二進で有限にならず、整数の最小単位や十進型と業務上の丸め規則が必要になる。
  • 印字した小数を厳密な保存値と思う。最短往復表記は 0.1 と表示しても、同じビットへ戻せる文字列を選んでいるだけだ。
  • オーバーフロー確認を省く。非トラップ加算は無限大を返し得る。必要に応じて事前の範囲検査や事後の isfinite を使う。C は <fenv.h>、NumPy は np.errstate などを提供し、他の演算や API の例外動作は異なる。

モデルの範囲

画面では binary16 と binary32 を使い、binary64 の fraction 52 ビットは逐列表示しない。加算は大きい指数へ合わせ、一度加え、正規化し、捨てた尾部で最後に一度丸める。実ハードウェアは guard、round、sticky ビットで境界を処理し、融合積和は乗算と加算の二操作を一度だけ丸められる。

減算と桁落ち、NaN 伝搬の詳細、他の丸めモード、例外状態レジスター、x87 の 80 ビット拡張は展開しない。厳密な動作は標準とプラットフォーム規則が定め、アニメーションは符号化と丸めの経路を理解するために使う。