← SEARCHABLE ARCHIVE
Computer Science · 2026-08-08

IEEE 754は実数をどのように保存するのか

binary32、binary64、丸め、NaN、累積誤差を理解する

INTERACTIVE READING MODE →

IEEE 754は実数をどのように保存するのか

この記事は講義を目的として作成しています。ハードウェア、コンパイラ設定、言語処理系によって細部は変わるため、重要な動作は実際の環境でも確認してください。

0.1 + 0.2が正確な0.3にならないと知り、計算機が計算を間違えたように感じることがある。しかし原因は加算能力ではなく表現方法だ。無限に存在する実数を有限個のビットパターンへ収めるため、多くの値は最も近い表現可能値として保存される。

IEEE 754はビット配置だけでなく、演算、丸め、無限大、NaN、gradual underflow、変換、例外状態を規定する。ここではbinary32とbinary64を分解し、0.1が近似になる理由と実務上の扱い方を説明する。

固定小数点と浮動小数点

固定小数点は小数点位置をあらかじめ決める。整数12345を常に小数二桁として扱えば値は123.45になる。一定の単位には便利だが、非常に小さい値と大きい値を同じ形式で扱う範囲は狭い。

浮動小数点は科学表記に似ている。

6.25 = 1.5625 × 2²
0.15625 = 1.25 × 2⁻³

仮数と指数を分けることで広い範囲を表現する代わりに、隣接する値の間隔は一定ではない。

IEEE 754が扱うもの

IEEE 754-2019は二進および十進の形式、演算、変換、丸め、例外条件と既定処理を定める。実装はハードウェア、ソフトウェア、または両方を利用できる。

名前よく対応する型全体符号指数保存fraction精度
binary32通常float32182324ビット
binary64通常double641115253ビット

型名だけですべての処理系に同じ形式が強制されるわけではない。C++ではstd::numeric_limits<T>::is_iec559などを確認できる。

binary32の三つの領域

符号1ビット | 指数8ビット | fraction 23ビット

通常の有限値は次の式で表せる。

(-1)^s × (1.fraction)₂ × 2^(E - 127)

指数は127のbiasを使う。正規化された二進仮数の先頭は必ず1なので保存せず、暗黙のビットとして扱う。そのため保存fractionが23ビットでも精度は24ビットになる。binary64のbiasは1023で、52ビットのfractionから53ビット精度を得る。

6.25を符号化する

6.25₁₀ = 110.01₂ = 1.1001₂ × 2²

符号は0、保存指数は2 + 127 = 129、二進では10000001になる。暗黙の1以降は1001000...0だ。

0 10000001 10010000000000000000000

16進数では0x40C80000である。

float value = 6.25f;
std::uint32_t bits = std::bit_cast<std::uint32_t>(value);

特別な指数パターン

指数fraction意味
全て0全て0符号付き0
全て00以外subnormal
1から254任意正規化有限値
全て1全て0正または負の無限大
全て10以外NaN

この予約により、binary32の正規化指数は-126から127になる。

正の0と負の0

指数とfractionが0でも符号ビットは残るため、+0と-0が存在する。通常の比較では等しいが、逆数などでは符号が現れる。

double p = 0.0;
double n = -0.0;

std::cout << (p == n) << '\n';
std::cout << std::signbit(n) << '\n';
std::cout << 1.0 / p << '\n';
std::cout << 1.0 / n << '\n';

一般的なIEEE動作では最後の二つは正と負の無限大になる。

subnormalとgradual underflow

0の近くで暗黙の1を使い続けると、最小正規化数と0の間に隙間ができる。保存指数が0でfractionが0でないときは次の式を使う。

(-1)^s × (0.fraction)₂ × 2^-126

精度を少しずつ失いながら0へ近づく値がsubnormalである。binary32の最小正規化正数は約1.17549435 × 10^-38、最小subnormal正数は約1.40129846 × 10^-45だ。

性能重視の環境ではsubnormalを0へflushするモードがある。SIMDやGPUの計算では設定を確認する必要がある。

無限大とNaN

指数が全て1でfractionが0なら無限大になる。オーバーフローや0での除算が既定処理のもとで無限大を生成する場合がある。

fractionが0でなければNaN、Not a Numberである。0.0 / 0.0や負数の実平方根などがNaNを作る。

NaNは順序付けできず、自分自身との等価比較も偽になる。

double nan = std::numeric_limits<double>::quiet_NaN();
std::cout << (nan == nan) << '\n';
std::cout << std::isnan(nan) << '\n';

NaNの検出にはstd::isnanを使う。quiet NaN、signaling NaN、payloadの対応は言語とハードウェアで異なる。

0.1が正確にならない理由

0.1は1 / 10である。既約分数が有限二進小数になるには分母の素因数が2だけでなければならない。10には5も含まれるため、二進表現が循環する。

0.1₁₀ = 0.000110011001100110011...₂

binary64は53桁分の有効ビットしか保存できず、残りを丸める。

std::cout << std::setprecision(20) << 0.1 << '\n';

典型的には0.10000000000000000555と表示される。0.1 + 0.2の差は、入力の保存時と加算結果の保存時に起こる丸めから生じる。

値の間隔は大きさで変わる

浮動小数点値は実数直線上に均等に並ばない。binary32は24ビット精度なので16,777,216までの整数を正確に表せる。その先では隣接値の差が2以上になる。

float a = 16'777'216.0f;
float b = a + 1.0f;
std::cout << (a == b) << '\n';

一般的なbinary32では真になる。この大きさではa + 1専用の表現がない。

epsilonは万能ではない

std::numeric_limits<double>::epsilon()は1と次の表現可能なdoubleの差であり、あらゆる大きさに使える許容誤差ではない。0付近の絶対誤差と、それ以外の相対誤差を組み合わせることが多い。

bool almostEqual(double a, double b,
                 double absoluteTolerance = 1e-12,
                 double relativeTolerance = 1e-9)
{
    double difference = std::abs(a - b);
    if (difference <= absoluteTolerance)
    {
        return true;
    }

    double scale = std::max(std::abs(a), std::abs(b));
    return difference <= scale * relativeTolerance;
}

許容値は座標、時間、物理など対象分野の単位と要件から決める。

丸めモードと例外状態

正確な結果が二つの値の間にあるとき、丸め方向が保存値を決める。一般的な既定値は最近接へ丸め、完全な中間なら最終ビットが偶数になる側を選ぶ方式である。

CとC++は<fenv.h>と<cfenv>で対応する丸め方向と状態フラグを扱う。

IEEEの五つの例外状態はinvalid operation、division by zero、overflow、underflow、inexactである。これは通常C++のthrowではなく、結果を生成して状態フラグを記録する仕組みだ。

std::feclearexcept(FE_ALL_EXCEPT);
volatile double one = 1.0;
volatile double zero = 0.0;
volatile double result = one / zero;

if (std::fetestexcept(FE_DIVBYZERO))
{
    std::cout << "division by zero flag\n";
}

演算順序と累積誤差

中間結果が毎回丸められるため、有限精度の加算は結合法則を満たさない場合がある。

double large = 1e16;
double small = 1.0;

double first = (large + small) - large;
double second = small + (large - large);

最初の式ではsmallが失われることがある。多数の値を加算する場合はKahan summationのような補償加算、似た大きさ同士の加算、より広い累算型を検討できる。

double kahanSum(const std::vector<double>& values)
{
    double sum = 0.0;
    double correction = 0.0;

    for (double value : values)
    {
        double adjusted = value - correction;
        double next = sum + adjusted;
        correction = (next - sum) - adjusted;
        sum = next;
    }
    return sum;
}

金額と正確な値

二進浮動小数点はグラフィックス、座標、多くの科学計算に適している。一方、正確な十進丸めが必要な金額には通常向かない。

  • 円やセントなど最小単位を整数で保存する。
  • decimalまたは固定小数点型を使う。
  • 丸めの時点と方向を業務規則として定める。

識別子や正確でなければならないカウンタも整数型を使う。

シリアライズ

ファイルやネットワークに保存するときは、単なる型名ではなく形式を定義する。

  • binary32かbinary64かを明記する。
  • 整数ビットパターンへ移してプロトコルのエンディアンを適用する。
  • 符号付き0とNaN payloadを保存する必要があるか決める。
  • テキスト往復には十分な桁数を出力する。

同じ型への十進往復にはC++のmax_digits10が利用できる。

実装を確認する

std::numeric_limitsでis_iec559、radix、digits、min()、denorm_min()、max()を確認できる。浮動型のmin()は最小の正の正規化値であり、最も小さい負数ではない。負側の下限にはlowest()を使う。

参考資料

まとめ

IEEE 754は符号、仮数、指数を使い、限られたビットで広い範囲を表す。その代わり多くの値が近似となり、間隔が大きさによって変わり、中間結果の丸めで演算順序の影響を受ける。この一つのモデルを理解すれば、0.1 + 0.2、NaN比較、大きな座標の揺れ、累積誤差を別々の不思議として扱わずに済む。