プログラムで使う数字には、表せる範囲があり、限界を超えると結果が壊れます。
本記事では、オーバーフローとアンダーフローの意味、起きる理由、そして初心者でもできる回避策を段階的に説明します。
整数と浮動小数点の違いから、具体例、検出と対処まで、基礎をやさしく押さえていきます。
数値表現の基本と範囲
ビット数と範囲(上限/下限)の考え方
コンピュータは数値をビット(0と1)の並びで表します。
ビット数が多いほど表せる数の種類が増え、上限と下限の幅が広がります。
例えば符号付き整数では、おおまかに-2^(n-1)から2^(n-1)-1までの範囲を持ちます。
符号なし整数の場合は0から2^n-1です。
次の表は、よく使う型の範囲イメージです(概略、言語や環境で異なることがあります)。
| 型の例 | ビット数 | 範囲の例(概略) |
|---|---|---|
| 符号付き整数(int8) | 8 | -128 〜 127 |
| 符号付き整数(int16) | 16 | -32768 〜 32767 |
| 符号付き整数(int32) | 32 | -2147483648 〜 2147483647 |
| 符号付き整数(int64) | 64 | 約-9.22e18 〜 約9.22e18 |
| 浮動小数点(float32) | 32 | 約±1.18e-38 〜 約±3.4e38 |
| 浮動小数点(float64) | 64 | 約±2.23e-308 〜 約±1.8e308 |
範囲の端を超えると、オーバーフローやアンダーフローが起きます。
この後で具体的に見ていきます。
整数と浮動小数点の違い
整数型は、範囲内であれば整数値を正確に表します。
対して浮動小数点型は、小数を含む広い範囲を表せますが、一部の数を近い値に丸めて表現します。
例えば0.1は2進数で有限桁にならないため、正確には表せません。
さらに浮動小数点には、無限大(Infinity)や非数(NaN)といった特別な値もあります。
丸めの影響と誤差のイメージ
丸めは保存時や計算中にも起こります。
代表例として、次の結果は多くの言語で見られます。
0.1 + 0.2 // 0.30000000000000004 など
これは計算が壊れているのではなく、表現の限界によるごく小さな誤差です。
繰り返し計算で誤差が蓄積したり、ほぼ同じ数の引き算で有効桁が失われたりすることがあるため、丸め誤差は起きうるものとして前提にすることが大切です。
オーバーフローとは?原因と例
定義
オーバーフローとは、計算結果の絶対値がその型で表せる上限を超え、別の値に化けてしまう現象です。
整数では桁が回って全く別の値になります。
浮動小数点ではInfinity(無限大)になることが一般的です。
整数のオーバーフローの例と症状
32ビット整数では最大値は2147483647です。
これに1を足すと、上限を超えて桁が回ります。
// Cの例
#include <stdio.h>
#include <limits.h>
int main() {
int x = INT_MAX; // 2147483647
int y = x + 1;
printf("%d\n", y); // -2147483648 に見える(桁回り)
}
桁回りの典型的な症状は、急に負の数に変わる、値が小さくなる、ループの終了条件が壊れるなどです。
符号なし8ビット(0〜255)でも255+1が0になります。
浮動小数点のオーバーフローの例
とても大きな数同士を掛けると、無限大に飛びます。
x = 1e308
y = x * 1e10
print(y) # inf (無限大)
infが出たらオーバーフローの合図です。
よくある原因
入力や計算結果が想定より大きくなる場面で起きます。
例えば、合計値をひたすら足し続ける、大きな数の乗算や累乗、型を小さい型へ変換、単位のミス(秒とミリ秒の取り違えなど)が原因になりやすいです。
オーバーフローの検出方法
最も確実なのは、演算の前に範囲をチェックすることです。
加算の例を擬似コードで示します。
// 符号付き32ビット整数の安全な加算(概念)
if (b > 0 && a > INT_MAX - b) then エラー
if (b < 0 && a < INT_MIN - b) then エラー
結果 = a + b
浮動小数点では計算後にisfiniteやisinfで確認します。
import math
z = x * y
if not math.isfinite(z): # infやnanを含む
print("範囲外の計算が発生しました")
結果がおかしい兆候(急な符号反転や極端な値)をログに出すのも有効です。
アンダーフローとは?原因と例
定義
アンダーフローとは、結果が0に近すぎて、その型では0として扱われるか、ほぼ0の特別な値になってしまう現象です。
整数の世界では下側の範囲外も「オーバーフロー」と呼ぶことが多く、ここでは主に浮動小数点の話を扱います。
浮動小数点でアンダーフローが起きやすい理由
浮動小数点には扱える最小の大きさがあります。
この最小値より小さい結果は0.0に丸められたり、極端に小さい近似値になったりします。
特にとても小さい数の掛け算や、大きい数での割り算を繰り返すと起きやすいです。
x = 1e-308
y = x * 1e-308
print(y) # 0.0 (doubleの範囲を下回り0に)
よくある原因
確率や正規化で値がどんどん小さくなる、非常に大きな数で割る処理、指数関数の大きな負の指数などが典型です。
ほぼ同じ数同士の引き算で有効桁が失われ、結果が極端に小さくなることもあります。
影響
本来は小さいけれど0ではない値が0.0に落ちると、ゼロ除算や分岐条件の誤作動につながります。
例えば分母が0になって無限大やNaNを生む、しきい値判定がすべて偽になる、といった予期せぬ挙動が発生します。
アンダーフローの検出方法
演算後に不自然な0.0が紛れ込んでいないかを確認します。
import math
tiny = 1e-300 # double向けの一例。用途に応じて調整
if x != 0.0 and y != 0.0:
z = x * y
if z == 0.0 or abs(z) < tiny:
print("アンダーフローの可能性があります")
また、ライブラリによっては非正規化やunderflowの警告設定が用意されています。
利用している言語やライブラリの「最小正規化値」を調べ、それ未満は0扱いにするか、スケールを変える方針を決めると良いです。
プログラミングの回避策とチェック方法
型選びと範囲の見積もり
最初に、扱う値のおおまかな最大値と最小値を見積もることが重要です。
カウント類はint64、広い桁の小数はfloat64、通貨は誤差を嫌うため10進小数型(Decimalなど)が向く場合があります。
PythonのintやJavaのBigIntegerのような任意精度も選択肢です。
| 用途の例 | 向くことが多い型 |
|---|---|
| 件数やID | 64ビット整数や任意精度整数 |
| 測定値や比率 | float64(倍精度) |
| 通貨 | Decimalや整数で最小通貨単位を扱う方式 |
範囲チェックとガード
演算の前に安全かどうかを判定すると事故が減ります。
加算や乗算は次のようにガードします。
// 乗算の一例(概念)
if (a != 0 && abs(b) > MAX / abs(a)) then エラー
結果 = a * b
浮動小数点は計算後にisfiniteで検査し、問題があれば例外やエラーログに回します。
演算順序の工夫とスケーリング
大きすぎる/小さすぎる中間結果を作らない工夫が効きます。
例えばa*b/cは、先にb/cを計算して値を小さくしてからaを掛ける方が安全な場合があります。
全体を一定係数で割ってから計算し、最後に元に戻すスケーリングも有効です。
指数が絡む場合は、対数で扱うとオーバーフローやアンダーフローを避けやすくなります。
大きすぎる/小さすぎる値のクランプ
入力や中間値を一定範囲にクランプ(はさみこみ)して暴走を防げます。
def clamp(x, low, high):
return max(low, min(x, high))
x = clamp(x, -1e6, 1e6) # 一例
ただし、クランプは本質的なバグを隠す場合があります。
警告ログやカウンタを付けて、発生頻度を監視しましょう。
安全なライブラリの利用
チェック付き算術や飽和算術(上限/下限で止める)を提供するライブラリを使うと安全です。
Pythonではdecimalやfractions、JavaではBigInteger/BigDecimalなどが代表例です。
数値計算ライブラリでは、エラー処理や警告設定を有効にしましょう。
例外/警告/アサーションで検出
想定外の値を早めに止めるため、境界チェックにアサーションや例外を使います。
import math
assert -1e12 <= x <= 1e12, "xが想定外の範囲です"
z = f(x)
if not math.isfinite(z):
raise ValueError("計算結果が有限数ではありません")
開発時に強めのチェック、本番ではログとサンプリングにすると、性能と安全性の両立がしやすいです。
テストで境界値と極端値を確認
境界値テストは最も効果的です。
上限直前、下限直前、ゼロ周り、非常に大きい値や小さい値を入力し、壊れないかを確認します。
浮動小数点ではinfやnanが出ないかも併せて見ます。
| 目的 | 入力例 | 期待する振る舞い |
|---|---|---|
| 上限直前の加算 | INT_MAXと1 | 事前チェックで弾くか例外 |
| 小さい数の乗算 | 1e-308と1e-308 | 0.0や極小値になったら警告 |
| 大きい数の乗算 | 1e308と1e10 | inf検出、エラー処理 |
| ほぼ同値の差 | 1.0000001と1.0 | 丸め誤差の説明を理解し許容範囲か判断 |
まとめ
本記事では、オーバーフローとアンダーフローの基本、原因、検出と回避の実践的ポイントを紹介しました。鍵は次の3点です。1) 型の範囲を理解し、事前に見積もる、2) 範囲チェックやisfiniteなどの検査を入れる、3) 演算順序やスケーリングで極端な中間値を避ける。
さらに、境界値テストで早期に問題を見つけ、必要に応じて安全なライブラリやクランプを組み合わせれば、初学者でも堅牢な数値処理が実現できます。
オーバーフローとアンダーフローを怖がるのではなく、仕組みを知り、手を打つことが大切です。
