計算結果がその型で表現できる範囲を超える現象。整数では値が反転し、浮動小数点では無限大やゼロになります。
身近な例で考えてみましょう。車のオドメーター(走行距離計)が999999kmを表示しているとき、あと1km走ると000000kmに戻ってしまいます。 6桁しか表示できないため、1000000という数を表現できないのです。コンピュータでもまったく同じことが起きます。
コンピュータでは数値を表現するビット数が決まっています。たとえば8ビット符号なし整数は0〜255の範囲しか表現できません。 この範囲を超える計算結果が出ると、正しい値を保持できなくなります。これがオーバーフロー(桁あふれ)です。 255に1を足すと256になるはずですが、8ビットでは256を表現できないため、結果は0になってしまいます。
上のツールで「255+1 (8bit符号なし)」プリセットを選ぶと、最大値を超えた瞬間に値が0に戻る(ラップアラウンドする)様子を環状数直線で確認できます。 ビット数を変えると表現範囲がどう変わるかも試してみてください。
整数のオーバーフローは時計の文字盤のように考えると分かりやすくなります。 12時の次は13時ではなく1時に戻りますよね。同じように、最大値の次は最小値(または0)に戻ります。 これをラップアラウンド(巻き戻り)と呼びます。
8ビット符号付き整数で具体的に見てみましょう。最大値は127(01111111)です。 ここに1を足すと、ビット列は10000000になります。これは2の補数表現で-128を意味します。 つまり 127 + 1 = -128 という、直感に反する結果になるのです。正の最大値から一瞬で負の最小値にジャンプしてしまいます。
上のツールの環状数直線はまさにこの「時計」を表しています。 4bitに設定して動かすと、少ない値で全範囲を一周する様子が見やすくなります。 符号付きモードに切り替えると、正から負への突然の変化も確認できます。
浮動小数点数(float/double)では、整数のようなラップアラウンドは起きません。 代わりに、最大値(約 1.8×10308)を超えるとInfinity(∞)という特殊な値になります。 整数が「時計のように一周する」のに対し、浮動小数点は「限界を超えると無限大になる」という違いがあります。
一度 ∞ になると、それ以降の計算も ∞ のままです(∞ + 1 = ∞、∞ × 2 = ∞)。 また、∞ - ∞ のような計算はNaN(Not a Number)という「数ではない」を表す特殊値になります。 NaN は何と比較しても false になる(NaN == NaN すら false)という独特な性質を持ちます。
上のツールで「浮動小数点」モードに切り替え、大きな値を入力して ∞ になる瞬間を確認してみてください。 「整数はラップ、浮動小数点は∞」という違いを押さえておくと理解が整理できます。
アンダーフローは、計算結果が0に非常に近い正の数になったとき、 浮動小数点数として正確に表現できなくなる現象です。最小の正規化数(約 2.2×10-308)より小さいと非正規化数になり、さらに小さいと0に丸められます。
オーバーフローが「大きすぎて溢れる」なら、アンダーフローは「小さすぎて潰れる」と覚えましょう。 たとえば 1.0 × 10-300 ÷ 1.0 × 1020 = 1.0 × 10-320 ですが、 これは double の最小正規化数より小さいため、精度が落ちるか0に丸められます。
科学計算や機械学習の分野では、アンダーフローで値が突然0になることで計算結果が大きく狂うことがあります。 対策として対数空間で計算する手法などが使われます。上のツールで「最小非正規化数」プリセットを試してみてください。
1996年6月4日、欧州宇宙機関(ESA)のアリアン5ロケットは打ち上げからわずか37秒後に空中で爆発しました。 原因はたった1行のコードにおける整数オーバーフローでした。 慣性航法装置(INS)が水平方向の速度データを64ビット浮動小数点数から16ビット符号付き整数に変換する際、 値が16ビットの範囲(-32768〜32767)を超えてしまったのです。
このコードはアリアン4の時代に書かれたもので、アリアン4では飛行速度が小さく16ビットに収まっていました。 しかしアリアン5は性能が大幅に向上し、水平速度がアリアン4の約5倍に達しました。 オーバーフローが発生すると慣性航法装置が停止し、バックアップ装置も同じコードを使っていたため同時に停止。 ナビゲーションデータが失われたロケットは急激に姿勢を崩し、自己破壊システムが作動して爆発しました。
この事故による損失は搭載されていた4基の衛星を含め約3.7億ドル(約500億円)に上りました。 「十分なビット幅を確保する」「型変換時に範囲チェックする」という基本的な対策があれば防げた事故であり、 ソフトウェアテストとオーバーフロー対策の重要性を示す教科書的な事例として今も語り継がれています。
| ビット数 | 符号なし | 符号付き |
|---|---|---|
| 4bit | 0 〜 15 | -8 〜 7 |
| 8bit | 0 〜 255 | -128 〜 127 |
| 16bit | 0 〜 65535 | -32768 〜 32767 |
| 32bit | 0 〜 4294967295 | -2147483648 〜 2147483647 |
符号なし整数(unsigned)は正の数だけを扱い、全ビットを値の表現に使います。 一方、符号付き整数(signed)は最上位ビット(MSB)を符号に使うため、負の数も扱えますが正の最大値は半分になります。 たとえば8ビットなら、符号なしは0〜255、符号付きは-128〜127です。
オーバーフローの振る舞いも大きく異なります。符号なしで255+1は0に戻りますが、符号付きで127+1は-128になります。 プログラムで「年齢」や「個数」のように負にならない値を扱うなら符号なしが安全ですが、「温度」や「座標」のように負を扱うなら符号付きが必要です。 上のツールで「符号付き/なし」を切り替えると、環状数直線の範囲が変わるのを確認できます。
オーバーフローを防ぐ主な方法は3つあります。 1つ目は計算前に範囲チェックする方法です。たとえば a + b を計算する前に 「a > MAX - b ならエラーにする」という事前検証を行います。最も確実な方法です。
2つ目はより大きなビット数の型を使う方法(int → long、32bit → 64bit)です。 3つ目は多倍長整数を使う方法で、Python の int は自動的に桁数が伸び、Java の BigInteger は任意精度の整数演算ができます。 ただし多倍長整数はメモリ消費が増え処理速度も遅くなるため、必要な場面で適切に選択することが重要です。
言語によって対策方法は異なります。JavaScriptでは BigInt 型、 Rustでは checked_add() メソッド(オーバーフロー時にNoneを返す)など、 安全な方法が用意されています。「オーバーフローの原因と対策」はセットで理解しておくと分かりやすく、上記3つの方法を押さえておきましょう。