浮動小数点の比較がなぜ危ないのか

浮動小数点数の比較は、2進数表現による微小な誤差のせいで=====が期待どおり動かない問題です。等値ではなく、差がごく小さいかどうかで判定するのが基本になります。

0.1 + 0.2 が 0.3 にならない

有名な話ですが、0.1 + 0.20.3 と厳密には一致しません。float は内部的に2進数で表現していて、0.1 や 0.2 は2進数で割り切れないため、ごくわずかな誤差が乗ります。画面には 0.3 と出ても、中身は 0.30000000000000004 だったりするわけですね。

var_dump(0.1 + 0.2 === 0.3); // bool(false)

// 誤差を見てみる
printf("%.17f\n", 0.1 + 0.2); // 0.30000000000000004

echo 0.1 + 0.2; // 表示上は 0.3 (丸められる)

「表示は正しいのに比較が通らない」という状況が生まれるので、原因に気づきにくいのが厄介なところだと思います。

== や === で比較しない

この誤差のせいで、float 同士を ===== で等値比較すると期待どおりに動かないことがあります。ループのカウンタを float で回して終了条件を === 1.0 にする、みたいなコードは特に危ういです。合計金額を float で持って残高と一致するか見る、というのも同じ罠を踏みます。

$sum = 0.0;
for ($i = 0; $i < 10; $i++) {
    $sum += 0.1;
}
var_dump($sum === 1.0); // bool(false) 直感に反する
printf("%.17f\n", $sum); // 0.99999999999999989

epsilonを使った近似比較

実務では「十分に近ければ等しいとみなす」という方針を取ります。二つの値の差の絶対値が、ごく小さな許容誤差(epsilon)より小さいかを見る形ですね。PHP_FLOAT_EPSILON という定数もありますが、扱う桁が大きいときは相対誤差で見るほうが安全な場合もあります。

function floatEquals(float $a, float $b, float $eps = 1e-9): bool
{
    return abs($a - $b) < $eps;
}

var_dump(floatEquals(0.1 + 0.2, 0.3)); // bool(true)

そもそも金額のように厳密さが要る値は float で持たず、整数(最小単位)や bcmath で扱うのが根本的な対策だと思います。

まとめ

float の等値比較は、2進数表現による誤差のせいで直感どおりには動きません。== / === ではなく、差が epsilon 未満かで判定するのが基本です。とはいえ近似比較は対症療法なので、お金や在庫のように誤差が許されない値は、整数化するか bcmath に逃がすのが本筋ですね。float は「だいたいの数」だと割り切って付き合うのが良い気がします。

よくある質問

Q. floatを==や===で比較してはいけないのですか。
A. 誤差で期待どおり動かないことがあるので避けたほうが安全です。差の絶対値がごく小さいか(epsilon未満か)で判定します。

Q. epsilonにはどんな値を使えばいいですか。
A. PHP_FLOAT_EPSILON などの小さな値を使いますが、扱う桁が大きいときは相対誤差で見るほうが安全な場合もあります。

Q. そもそも誤差を出したくないときは。
A. 金額のように厳密さが要る値はfloatで持たず、整数(最小単位)やbcmathで扱うのが根本的な対策です。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です