浮動小数点は安全じゃない
浮動小数点数は誤差が出て安全じゃないから、重要な計算の時は使っちゃいけないよという話。
PHP に限った話ではなく、コンピュータ全般。
若手エンジニアからこんな質問がきた。
「5761800 を 小数切り捨てしたら 5761799 になっちゃった!」
<?php $rate = 4.850/100; $result = 1188000 * $rate; echo $result*100; // 5761800 echo floor($result*100), "\n"; // 5761799
浮動小数点数には以下のような特徴があり、誤差が発生する。
誤差の許されないような重要な計算には使うことができず、
そういう場合には 整数 もしくは 固定小数点数 で行う必要がある。
- 有限桁(53)による近似値への丸め
- 値の近い二値の減算で桁落ち発生
- 絶対値の大きな値と小さな値のかさんで情報落ち発生
他にも、緩和策がある
- 浮動小数点数でも、近似値に丸められない値を使う
- 誤差の発生しうる計算を後回しにする(誤差を小さくする)
- 主に除算
実際の値はこうなっている
<?php $rate = 4.850/100; // 0.0485 $result = 1188000 * $rate; // 57617.99999999999272404238581657409667968750000000000000000 echo $result*100; // 5761799.99999999906867742538452148437500000000000000000000000 // 文字列キャストで少数点以下が丸められ、 // 5761800 echo floor($result*100), "\n"; // floor(5761799.99xxx) = 5761799.0
解決策1. 整数で計算する
但し今度はINTのオーバフローに注意しなければならない。
具体的には BCMath や GMP が必要になる。
<?php $rate = 4850; // 100,000 倍して整数にした $result = 1188000 * $rate; // 5761800000 echo $result*100 /100000; // 5761800 echo floor($result*100 /100000), "\n"; // 5761800
GMP を使うとこうなる
<?php $rate = gmp_init(4850); $result = gmp_mul(gmp_init(1188000), $rate); echo floor(gmp_strval(gmp_div_q(gmp_mul($result, gmp_init(100)), gmp_init(100000)))), "\n"; echo gmp_strval(gmp_div_q(gmp_mul($result, gmp_init(100)), gmp_init(100000)));
解決策2. 固定小数点数で計算する (PHPでは無理)
PHPには固定小数点数型は存在しない。
保持するだけなら文字列で持っておけばよいが、計算時は必ず浮動小数点数になってしまう。
固定小数点数への変換自体は、sprintf() で文字列にキャストすればよい。
指定桁数内で丸められ、固定小数点数の文字列になる。
ceil() / floor() / round() と組み合わせれば、ある程度の精度にはなる。
sprintf('%.53f', $floatVal);
少数切り捨てという今回の仕様なら満たせそうだけど、
$rate と $result で2回丸めが発生しているため誤差は出ている。
<?php $rate = 4.850/100; // 0.04849999999999999450439602810547512490302324295043945 $result = 1188000 * $rate; // 57617.99999999999272404238581657409667968750000000000000000 $result = sprintf('%.0f', $result); // 小数第1位で丸められ、57618 echo floor($result*100), "\n"; // floor(5761800) = 5761800 echo $result*100; // 5761800
緩和策1. 誤差の発生しうる計算を後回しにする
そもそも $rate を 100除算して、$result を100乗算している意味はあるのか?
100 がユーザ入力なのか固定値なのか仕様は不明だが、固定値だとしたら消すことができる。
すると、$rate の誤差が小さくなり、$result の誤差はわからなくなった。
<?php $rate = 4.850; // 4.84999999999999964472863211994990706443786621093750000 $result = 1188000 * $rate; // 5761800.00000000000000000000000000000000000000000000000000000 echo floor($result), "\n"; // 5761800 echo $result; // 5761800