浮動小数点数(float, double)はたいていの場合で使えない

PHPにかぎらず、float型とdouble型はたいていの場合で使えません。値が不正確だからです。

論より証拠

ググればfloatがダメな理由はいっぱい出てくるのですが、論より証拠です。

<?php

$int = 0;
$float = 0.0;
for ($i = 0; $i < 10000000; $i++) {
    $rand = mt_rand(1, 1000);
    $int   += $rand;
    $float += $rand / 10;
}

echo "int:   {$int}\n";
echo "float: {$float}\n";

ランダムな値を、1つは整数で、1つは10で割って小数点第1位までの小数にして足し算をひたすら繰り返します。
実際の実行結果は次のようになります。

$ php test.php
int:   5006861668
float: 500686166.80001

$ php test.php
int:   5005397692
float: 500539769.19995

$ php test.php
int:   5003787607
float: 500378760.69994

わずかに違う値です。

そうは言ってもだいたい動くじゃん?

これのループ回数を1000回ぐらいに抑えると、intとfloatで一致するようになります。
なら実用の範囲じゃないか? という考え方もあるかもしれませんが、floatを使うことで再現性の低いバグが埋め込まれることになります。仕事でもホビープログラムでも、好ましいことではないでしょう。

また、この挙動はPHPに限らず、データベースでもfloat・doubleを使うと同じようにわずかに狂います。

代替案1:ライブラリを使う

float型の精度の悪さが問題になることは多いため、PHPでは任意精度数学関数GMP関数が用意されています。

ただ、これらのライブラリを使ってもデータベース上でsum()したいとかになると対応できません。

代替案2:整数で疑似的に「固定小数」を使う

小数は誤差がありますが、整数ならオーバーフローしないかぎり正確です。「この変数は小数点第2位までしか使わない」と分かっている場合は100倍した値を使う――つまり整数にしてしまえばズレは起きません。表示するときだけ次のように小数点を割り込ませます。

printf("%d.%02d", substr($var, 0, strlen($var) - 2), $var % 100);

注意点として、「小数点を第3位まで取り扱う」という仕様変更が加わると修正が大変なことになります。桁数の決定には慎重な注意が必要です。

代替案3:割り算は最後に

上記例は「割ってから足す」をしています。割り算の結果はfloat型になりますので、早いうちから不正確になります。intは正確に計算できていますので、intで足してから最後に10で割れば比較的安全です。所詮float型は精度が悪いので桁数が増えるとやっぱりダメなんですが、代替案2ほどの厳密さが不要な場合には使えるでしょう。

コメントを残す

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

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください