hnwの日記

PHP5.3.4から5.3.6までのround関数がときどき丸めすぎてたのが直った

PHP5.3.4から5.3.6のround関数に問題があるんじゃないか、と僕がバグ報告していた件(PHP :: Bug #54334)が、PHP5.3.7から修正されています。僕のバグレポには特に返事もないので、全く独立に修正されたんだと思います。PHP5.3.7のChangeLogには次のような記述があります。

Alternative fix for bug Fixed bug #52550, as applied to the round() function (signed overflow), as the old fix impacted the algorithm for numbers with magnitude smaller than 0. (Gustavo)


PHP: PHP 5 ChangeLog


これはどんなバグかというと、round関数で1.0未満の数を丸める際にpre-roundingを行う桁が狂っており、丸めすぎてしまうことがあるというものです。


pre-roundingというのは、丸めたい桁で丸める前にまず精度ギリギリの桁で丸めを行うという処理のことで、PHP5.3以降のround関数で採用されています。これは僅かな誤差を許容することで丸めに関するトラブルを防ぐ意図だと言えます(参照:「PHP5.3.0alpha3のround関数の実装がPHP5.2.6と変わった」)。ところが、今回のバグは特定の条件下でpre-roundingを不適切な桁で実施してしまうというものでした。


この問題は、次のようなプログラムで確認できます。

<?php
var_dump(round(0.000000000000445,13));


小数点のあとに0が12個続いて、そこから445と3桁続くような小数を小数点以下13位に丸めるという例です。14桁目が4ですから当然切り捨てになるはずですが、PHP5.3.4-5.3.6では切り上げになってしまいます。

$ php-5.3.3 /tmp/php5.3.6-round-bug.php
float(4.0E-13)
$ php-5.3.6 /tmp/php5.3.6-round-bug.php
float(5.0E-13)
$ php-5.3.7 /tmp/php5.3.6-round-bug.php
float(4.0E-13)


何が起きているか説明すると、PHP5.3.4-5.3.6ではまず小数点以下15位を四捨五入で切り上げて小数点以下14位を5にしてから(pre-rounding)、改めて小数点以下14位で四捨五入を実施します。この不適切なpre-roundingのおかげで、再度切り上げが起こって小数点以下13位が5になってしまうというわけです。


こんな実装になっていた理由は謎ですが、別のバグを直そうとして見当外れなコードをcommitしたというのが僕の想像です。実際、PHP5.3.7からは僕の期待通り、PHP5.3.3以前と同様の実装に戻っています。


本バグの実案件への影響はほぼ無いと思います。本来pre-roundingは10進の有効桁数15桁目を丸める処理なのですが、このバグのせいで丸め対象が1.0未満なら常に小数点以下15位を丸めていました。つまり、小数点以下14位あたりの桁が重要な場合に限って影響があるというわけです。万一心当たりがある方はPHP5.3.8へのバージョンアップを検討すると良いでしょう。


また、round関数の議論に興味がある場合、PHP5.3.4から5.3.6は議論の対象から外すのが良いと思います。例えばround関数の実装の説明をする場合であれば、PHP5.3.4から5.3.6に注目するのは無意味だというわけです。今ならPHP5.3.8の説明だけで十分です。


以上、次回以降の記事の予防線でした。