オーバーフローとアンダーフロー

この章の目標

オーバーフローアンダーフローの定義を理解し、IEEE 754 での扱いと非正規化数の役割を学び、典型的な発生場面と回避技法を身につける。

前提知識

IEEE 754 浮動小数点数の基本構造

目次

1. オーバーフロー

オーバーフロー(Overflow)とは、演算結果の絶対値が浮動小数点数で表現可能な最大値を超える現象である。

$$|x| > x_{\max} \quad \Rightarrow \quad \text{オーバーフロー}$$
精度最大値 $x_{\max}$最大指数
単精度(float32)$\approx 3.4 \times 10^{38}$$2^{127}$
倍精度(float64)$\approx 1.8 \times 10^{308}$$2^{1023}$

IEEE 754 ではオーバーフロー時に結果は $\pm\infty$(無限大)にセットされる。$\infty$ に対する演算は $\infty + 1 = \infty$, $\infty \times 2 = \infty$, $\infty - \infty = \text{NaN}$ などの規則に従う。

倍精度の正確な限界値

  • 最大正規化数: $(2 - 2^{-52}) \times 2^{1023} \approx 1.7976931348623157 \times 10^{308}$
  • 最小正規化数: $2^{-1022} \approx 2.2250738585072014 \times 10^{-308}$
  • 最小非正規化数: $2^{-1074} \approx 4.9406564584124654 \times 10^{-324}$

指数部は 11 ビット(バイアス 1023)で、指数 $2^{1024}$ 以上は $\infty$ として扱われる。

2. アンダーフロー

アンダーフロー(Underflow)とは、演算結果の絶対値がゼロでないにもかかわらず、表現可能な最小正規化数より小さくなる現象である。

$$0 < |x| < x_{\min} \quad \Rightarrow \quad \text{アンダーフロー}$$
精度最小正規化数 $x_{\min}$最小非正規化数
単精度(float32)$\approx 1.2 \times 10^{-38}$$\approx 1.4 \times 10^{-45}$
倍精度(float64)$\approx 2.2 \times 10^{-308}$$\approx 5.0 \times 10^{-324}$

アンダーフローはオーバーフローに比べて致命的ではないことが多い。IEEE 754 の段階的アンダーフローにより、結果はゼロに「突然フラッシュ」されるのではなく、精度を徐々に失いながらゼロに近づく。

3. IEEE 754 における扱い

0 x_min x_max 非正規化数 正規化数 +∞ −x_min −x_max 非正規化数 正規化数 −∞ 段階的アンダーフロー オーバーフロー オーバーフロー
図1. 浮動小数点数の表現範囲(正負対称)。0 付近では非正規化数により段階的アンダーフローが起き、$\pm x_{\max}$ を超えると $\pm\infty$ にオーバーフローする。

IEEE 754 の特殊値:

  • $\pm\infty$: オーバーフローの結果。$1/0 = +\infty$, $-1/0 = -\infty$ なども含む。
  • NaN(Not a Number): $0/0$, $\infty - \infty$, $\sqrt{-1}$ など、定義されない演算の結果。
  • $\pm 0$: 正のゼロと負のゼロが区別される。$1/(+0) = +\infty$, $1/(-0) = -\infty$ となる。

4. 非正規化数(デノーマル数)

非正規化数(denormalized number / subnormal number)は、指数部が最小値のときに仮数部の暗黙の先頭ビット1を0とすることで、$x_{\min}$ より小さい数を表現する仕組みである。

非正規化数により、$x - y = 0 \Leftrightarrow x = y$ という重要な性質が保証される(アンダーフローのフラッシュ・トゥ・ゼロ方式ではこの性質が壊れる)。

ただし、非正規化数の有効桁数は正規化数より少なく、一部のプロセッサでは非正規化数の演算が著しく遅い(数十〜100 倍以上)という性能上の問題がある。

非正規化数の性能ペナルティ

x86 アーキテクチャの多くで、非正規化数を含む演算はマイクロコード処理にフォールバックし、正規化数の数十〜100 倍程度の遅延が観測される報告がある(プロセッサ世代に強く依存)。リアルタイム処理・DSP・オーディオ・物理シミュレーションでは、IEEE 754 厳密性と引き換えに FTZ(Flush-to-Zero)を有効化することが多い。

// SSE 経由で FTZ + DAZ を有効化(GCC/Clang/MSVC 共通)
#include <xmmintrin.h>
#include <pmmintrin.h>
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);

5. 典型的な発生場面

オーバーフロー

  • 階乗: $170! \approx 7.26 \times 10^{306}$ は倍精度の範囲内であるが、$171! \approx 1.24 \times 10^{309}$ でオーバーフローする。
  • 指数関数: $e^{709} \approx 8.2 \times 10^{307}$ は倍精度の範囲内であるが、$e^{710}$ でオーバーフローする。
  • ベクトルのノルム: $\|x\|_2 = \sqrt{\displaystyle\sum x_i^2}$ で $x_i$ が大きいと $x_i^2$ でオーバーフローする。

アンダーフロー

  • 確率の積: 多数の小さい確率の積はアンダーフローしやすい。対数空間で計算する(対数確率)。
  • 指数関数の減衰: $e^{-x}$ で $x > 745$ (倍精度)ではアンダーフローしてゼロになる。
  • ガウス分布の密度関数: 遠い点での $e^{-x^2/2}$ は非常に小さい。

6. 回避技法

対数スケールでの計算

積を和に変換する。$\displaystyle\prod p_i$ の代わりに $\displaystyle\sum \log p_i$ を計算する。最終結果が必要なときだけ $\exp$ を適用する。

Log-Sum-Exp トリック

$\log\left(\displaystyle\sum_i e^{x_i}\right)$ を安定に計算するために、$M = \max_i x_i$ として $e^M$ を共通因子として外に出す:

$$\log\left(\displaystyle\sum_i e^{x_i}\right) = \log\left(e^M \displaystyle\sum_i e^{x_i - M}\right) = M + \log\left(\displaystyle\sum_i e^{x_i - M}\right)$$

$x_i - M \le 0$ であるから $e^{x_i-M} \le 1$ となりオーバーフローしない。また少なくとも 1 項は $e^0 = 1$ なので $\log$ の中身は $[1, n]$ に収まり、アンダーフローによる $\log 0 = -\infty$ も防げる。

スケーリング(正規化)

ベクトルのノルム $\|x\|_2$ を計算する際、$m = \max_i |x_i|$ で割ってから計算する。

$$\|x\|_2 = m \sqrt{\displaystyle\sum_i (x_i / m)^2}$$

$(x_i/m)^2 \le 1$ であるからオーバーフローしない。LAPACK の dnrm2 はこの手法を用いている。

式の変形

$e^a / e^b$ は $e^{a-b}$ に、$\sqrt{a} \cdot \sqrt{b}$ は $\sqrt{ab}$ に変形すると中間値のオーバーフロー・アンダーフローを回避できることがある。$\sqrt{a^2 + b^2}$ には専用関数 hypot(a, b)(IEEE 754-2008 規格)を用いる。

7. 計算例

例1: ソフトマックスでの Log-Sum-Exp トリック

分類問題で $x = (1000, 1001, 1002)$ に対しソフトマックスを計算する場合を考える。素朴に

$$s_i = \dfrac{e^{x_i}}{\displaystyle\sum_j e^{x_j}}$$

を倍精度で実行すると $e^{1000} \approx 1.97 \times 10^{434}$ で各項がオーバーフローし、$\infty / \infty = \text{NaN}$ となる。$M = \max_j x_j = 1002$ で平行移動すると

$$s_i = \dfrac{e^{x_i - M}}{\displaystyle\sum_j e^{x_j - M}} = \dfrac{(e^{-2},\, e^{-1},\, e^{0})_i}{e^{-2} + e^{-1} + 1} \approx (0.0900,\, 0.2447,\, 0.6652)_i$$

となり、全ての中間値が $(0, 1]$ に収まる。理論的にも変換前後で値は等しい。Python での実装例:

import math

def softmax(x):
    m = max(x)
    exp_x = [math.exp(xi - m) for xi in x]
    s = sum(exp_x)
    return [e / s for e in exp_x]

print(softmax([1000, 1001, 1002]))
# [0.09003057317038046, 0.24472847105479764, 0.6652409557748219]

例2: ベクトルノルムのスケーリング

$x = (10^{200},\, 10^{200})$ に対し $\|x\|_2 = \sqrt{x_1^2 + x_2^2}$ を素朴に計算すると、$x_1^2 = 10^{400}$ で倍精度範囲($\sim 10^{308}$)を超え、結果は $\infty$ となる。最大成分 $m = 10^{200}$ で正規化すると

$$\|x\|_2 = m \sqrt{(x_1/m)^2 + (x_2/m)^2} = 10^{200} \sqrt{1 + 1} = \sqrt{2} \cdot 10^{200} \approx 1.414 \times 10^{200}$$

と正しく計算できる。真値も $10^{308}$ 未満であり、表現可能である。

例3: hypot による安全な計算

同じく $\sqrt{a^2 + b^2}$ の安全な計算には IEEE 754-2008 で標準化された hypot(a, b) を用いる。例えば C の hypot(1e200, 1e200) は内部でスケーリングを行うため $\sqrt{2} \cdot 10^{200}$ を直接返す。同様に std::expm1(x)($e^x - 1$)や std::log1p(x)($\log(1+x)$)は $|x| \ll 1$ での精度低下とアンダーフローを同時に避ける。

8. 注意点

  • FTZ / DAZ モード: 性能上の理由から SSE/AVX では FTZ(Flush-to-Zero、結果の非正規化数を 0 にする)や DAZ(Denormals-Are-Zero、入力の非正規化数を 0 として扱う)が有効化されている場合がある。これにより段階的アンダーフローが無効になり、$x \ne y$ でも $x - y = 0$ となる可能性がある。
  • 非正規化数の性能ペナルティ: 多くの x86 プロセッサで非正規化数を含む演算はマイクロコード処理に切り替わり、100倍以上遅くなることがある。リアルタイム処理や DSP では FTZ を意図的に有効化することが多い。
  • 符号付きゼロの比較: $+0 = -0$ は true となるが、$1/(+0) = +\infty$, $1/(-0) = -\infty$ で逆数を取ると $+\infty \ne -\infty$ となり、分岐結果が変わる。
  • 中間値オーバーフロー: 最終結果が表現範囲内でも、中間計算でオーバーフローする例は多い($\sqrt{a^2 + b^2}$、$(a \cdot b)/c$ など)。式の評価順序の入れ替えや専用関数の利用を検討する。
  • 整数オーバーフローとの混同: 整数型のオーバーフローは IEEE 754 の対象外で、C/C++ では符号付き整数オーバーフローは未定義動作である。本記事は浮動小数点に限定する。

オーバーフロー・アンダーフローの検出

IEEE 754 ではオーバーフロー・アンダーフローはサイレントに発生するため、明示的に検出するには浮動小数点例外フラグを参照する。C99 / C++11 では <fenv.h> / <cfenv> を用いる:

#include <cfenv>
#pragma STDC FENV_ACCESS ON

std::feclearexcept(FE_ALL_EXCEPT);
double r = dangerous_computation();
if (std::fetestexcept(FE_OVERFLOW))  { /* +/-inf 発生 */ }
if (std::fetestexcept(FE_UNDERFLOW)) { /* 非正規化数 or 0 */ }

SIGFPE をトラップする方式(feenableexcept on glibc)もあるが、移植性とパフォーマンスに難があり、ライブラリ境界での状態管理が必要になる。

9. よくある質問

Q1. オーバーフローとは何ですか?

演算結果の絶対値が浮動小数点数で表現可能な最大値を超える現象である。IEEE 754 では結果は $\pm\infty$ となる。倍精度の最大値は約 $1.8 \times 10^{308}$ である。

Q2. アンダーフローとは何ですか?

演算結果の絶対値がゼロでないにもかかわらず、表現可能な最小正規化数より小さくなる現象である。IEEE 754 の段階的アンダーフロー(非正規化数)により、精度は低下するがゼロへの突然のフラッシュは避けられる。

Q3. オーバーフローやアンダーフローを回避する方法は何ですか?

対数スケールでの計算(log-sum-exp など)、式の変形による中間値の安定化、スケーリング、拡張精度演算の使用などがある。

10. 参考資料

  • Wikipedia「算術オーバーフロー」(日本語版)
  • Wikipedia「Arithmetic underflow」(英語版)
  • Wikipedia「Denormal number」(英語版)
  • D. Goldberg, "What Every Computer Scientist Should Know About Floating-Point Arithmetic," ACM Computing Surveys, vol. 23, no. 1, pp. 5--48, 1991.
  • N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM, 2002(§2.7 にオーバーフロー・アンダーフローの解析、§27 に hypot 等の安全な計算)。
  • IEEE 754-2019, IEEE Standard for Floating-Point Arithmetic, IEEE, 2019.

sangi での実装

多倍長浮動小数点数 sangi の Float クラス は指数部を 64 ビット整数で保持するため、IEEE 754 倍精度の $\pm 10^{308}$ という範囲制約を受けず、$10^{10^{18}}$ オーダーまで表現できる。本記事で扱った「中間値オーバーフロー」の多くは、最終結果まで多倍長で計算すれば自然に解消される。