メインコンテンツまでスキップ

陥りやすい落とし穴とその回避策

浮動小数点数(floatやdouble)はコンピュータ上で実数を扱うために不可欠なデータ型ですが、内部的には2進数で近似的に表現されるため、思わぬ誤差やバグを引き起こすことがあります。本記事では、浮動小数点演算においてよく見られる問題とその回避策について、実例とともに解説します。

乗除算の順序による丸め誤差の伝播

浮動小数点数の演算では、演算の順序によって結果に微妙な違いが生じることがあります。特に、乗算と除算を組み合わせた式では、中間結果の丸め誤差が後続の演算に伝播し、期待した結果と異なる値になることがあります。

例えば、a-a*b/b は数学的には常に 0 になるはずですが、a に丸め誤差が含まれている場合、a * b の結果が正確でなくなり、/ b の除算で誤差が拡大されることがあります。

decimal a = 10m / 3m; // 丸め誤差を含む
decimal b = 5m;
decimal result = a - a * b / b;
Console.WriteLine(result); // 0 ではない(誤差が残る)
// [出力例]
// 0.0000000000000000000000000001

対策: 計算順序の工夫による誤差の抑制

このような場合、除算を先に行うことで誤差の影響を軽減できることがあります。

decimal a = 10m / 3m;
decimal b = 5m;
decimal result = a - a * (b / b); // b / b は 1.0 に丸められる
Console.WriteLine(result); // より安定して 0 に近い結果が得られる
// [出力例]
// 0.0000000000000000000000000000

ただし、このような変形は、b ≠ 0 であることが保証されている場合に限ります。扱う数値の性質やスケールを理解した上で、演算順序を工夫することが重要です。

配列やリストでの合計値の誤差蓄積

大量の浮動小数点数を加算していくと、誤差が蓄積していき、最終的な合計値が大きくずれることがあります。

#include <iostream>
#include <vector>

int main() {
    std::vector<float> values(1000000, 0.0001f);
    float sum = 0.0f;
    for (float v : values) {
        sum += v;
    }
    std::cout << "Sum: " << sum << std::endl; // 本来100.0になるはずが誤差が出る
}

// [出力例]
// Sum: 99.3273

対策:Kahan加算アルゴリズムの導入

Kahan加算アルゴリズム(Kahan Summation Algorithm)は、浮動小数点数の加算における丸め誤差を低減するための数値計算手法です。通常の加算では、小さな値が大きな値に加算されるときに丸め誤差が発生しやすく、これが繰り返されることで合計値に大きな誤差が生じます。

Kahan加算では、補正項(compensation)を導入することで、失われた精度を追跡し、次の加算に反映させることができます。

アルゴリズムの概要

  1. sum に加算結果を保持
  2. c に補正項(前回の丸め誤差)を保持
  3. 各加算時に、補正項を差し引いた値を加算
  4. 新たに発生した誤差を c に記録
#include <iostream>
#include <vector>

int main() {
    std::vector<float> values(1000000, 0.0001f);
    float sum = 0.0f;
    float c = 0.0f;
    for (float v : values) {
        float y = v - c;
        float t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }
    std::cout << "Kahan Sum: " << sum << std::endl;
}

// [出力例]
// Kahan Sum: 100

for ループでの浮動小数点インクリメントによる無限ループ

浮動小数点数でループ変数をインクリメントすると、誤差により終了条件が満たされず、無限ループになることがあります。

for (float x = 0.0f; x != 1.0f; x += 0.1f) {
    std::cout << x << std::endl; // 無限ループになる可能性あり
}

対策:整数ベースのループ制御

for (int i = 0; i <= 10; ++i) {
    float x = i * 0.1f;
    std::cout << x << std::endl;
}