Hatena::ブログ(Diary)

hnwの日記 このページをアンテナに追加 RSSフィード

[プロフィール]
 | 

2007年5月15日(火) PHPの奇妙なround関数 このエントリーを含むブックマーク このエントリーのブックマークコメント

(2012/11/01追記) 4年ほど前の記事「PHP5.3.0alpha3のround関数の実装がPHP5.2.6と変わった - hnwの日記」でお伝えした通り、PHP 5.3.0から別の実装が採用されており、本ページで指摘しているような挙動のPHPは既に絶滅危惧種です。念のため。


さて、プログラミングの話題もたまには書いてみます。今回はPHPround関数の挙動が変だ!という話題です。


round()は浮動小数点数を四捨五入する関数で、大抵の言語に同じ名前で実装されているかと思います。ではPHPのround関数の何が問題なのか、ちょっと試してみましょう。

$ uname -sro
Linux 2.6.9-42.0.10.plus.c4smp GNU/Linux
$ php --version
PHP 5.1.6 (cli) (built: Feb 23 2007 06:56:38)
Copyright (c) 1997-2006 The PHP Group
Zend Engine v2.1.0, Copyright (c) 1998-2006 Zend Technologies
$ php -r '$x1=0.49999999999;$x2=0.5;var_dump($x1,$x2,($x1===$x2),round($x1),round($x2));'
float(0.49999999999)
float(0.5)
bool(false)
float(1)
float(1)
$

上記の通り、0.49999999999を四捨五入したら1になってしまいました。ここでクイズです。上記のような結果になった理由は何でしょうか。


上の結果を見た瞬間に「ああ、よくある浮動小数点数の精度とか誤差とかの話題か」と思った方は一定以上経験のあるプログラマなのだろうと思いますが、残念ながらハズレです。0.5は2進で正確に表せる数ですから、丸め誤差が発生するわけでもありません。また、PHP浮動小数点数はCでいうdoubleそのものですから、10進11桁程度であればそれなりの精度で格納できます。


さて、答え合わせの前に、他の言語でも試してみることにしましょう。Rubyだとどうなるでしょうか。

$ ruby -e '$x1=0.49999999999;$x2=0.5;p($x1,$x2,($x1==$x2),$x1.round(),$x2.round());'
0.49999999999
0.5
false
0
1                   
$

なるほど。これは直感通りの結果です。念のためCでも試してみましょうか。

$ cat /tmp/round-test.c
#include<stdio.h>
#include<math.h>

int main() {
  double x1=0.49999999999, x2=0.5;
  printf("%.11f\n%.11f\n%d\n%f\n%f\n",
         x1, x2, x1==x2, round(x1), round(x2));
  return 0;
}
$ gcc -lm /tmp/round-test.c
$ ./a.out
0.49999999999
0.50000000000
0
0.000000
1.000000
$

やはり普通はそうなるはずですよね。


PHPソースコードを少し眺めていると、すぐに不穏な個所にぶつかりました。

#define PHP_ROUND_WITH_FUZZ(val, places) {                      \
        double tmp_val=val, f = pow(10.0, (double) places);     \
        tmp_val *= f;                                   \
        if (tmp_val >= 0.0) {                           \
                tmp_val = floor(tmp_val + PHP_ROUND_FUZZ);      \
        } else {                                        \
                tmp_val = ceil(tmp_val - PHP_ROUND_FUZZ);       \
        }                                               \
        tmp_val /= f;                                   \
        val = !zend_isnan(tmp_val) ? tmp_val : val;     \
}                                                       \

どうやらこのマクロPHPのround関数の実体のようです。不思議なことに、ライブラリ関数のround(3)を呼ばずに謎の定数PHP_ROUND_FUZZとfloor(3)とceil(3)を使ってround関数らしきものを実装しているようです。round(3)を避ける意味がわかりません。更に不安なことに、少なくとも僕の手元の環境では定数PHP_ROUND_FUZZは0.50000000001なんていう不思議な数に定義されているみたいですよ!こんな見事なマジックナンバーは久々に見ました!ステキすぎてクラクラしちゃいますね!


皮肉はさておき下記のURLを読んでみると、こんな実装になっている理由がわかったような気がします。

この一連のバグ報告を斜め読みで要約すると、「紙とペンで計算すると5.045になるはずの値(実際にはコンピュータ上では約5.04499999999999992894573)を小数点以下第二位までで四捨五入してるのになぜか5.04になった!バグだ!」って騒いでいるプログラマバグ報告をしてきて、これに対処するために四捨五入の境界値付近(0.00000000001くらいの差)だったら全部0から遠い方に切り上げるようなコード修正をした、ということかと思います。他の言語なら無知なバグ報告者を罵倒して終わるはずのところを、バグ修正として対応してしまうところがPHPらしいのかもしれません。もっとも、これは想像なので実際どうだか知りませんけどね。もし詳しい人がいらしたら教えてください。


というわけで、このエントリの解答としては「どうやらPHPの仕様」ということになります。実は僕が調べ始めた時点での予想は「PHPバグ」でしたので、これは僕自身にとっても意外な結果です。


蛇足になりますが、http://jp.php.net/manual/ja/function.round.php の先頭には英語版には付いていない注意書き

(訳注:内部的な 2 進数表現と 10 進数表現の差により生じる丸め誤差の影響により 必ずしも小数点以下を四捨五入した結果を返さないことに注意してください。)

が書いてあります。これは訳者の優しさが表れている文章だと思います。一方で、今回のroundの妙な挙動が本当にPHPの仕様だとしたら、PHP中の人は優しさが無いと思うんですよね。それとも、これはこれで優しさなんでしょうかね?


追記:ここに直接飛んでくる人も多そうなのでご案内を。下記の通り、実はこの話題は延々と続いているのですが、まだまとめきれていません。おそらく重要な話題は、その3「PHP_ROUND_FUZZが0.50000000001と定義されるかどうかは環境次第、多くのLinux環境が該当する」および、その6「PHPは無知なクレーマーへの対策としてバグ修正をしたわけではない」「本件をポータビリティに関するバグとみなして修正すること自体は特に問題がない」といった点だと思います。「PHP以外全員不正解」はround関数とは別の話題ですが、大抵の言語の実装者でさえ浮動小数点数のプロではない、という傍証と言えるかもしれません。それでも誰も困っていないんだから、正確性を議論するなんて無意味なんじゃないでしょうか。また、「PHP5.3.0alpha3のround関数の実装がPHP5.2.6と変わった」の通り、PHP5.3.x系から上記実装は廃止され、新たな実装が採用される予定です。

 

 2007-05-15 PHPの奇妙なround関数

 2007-05-30 round関数で整数を四捨五入してみる

 2007-06-03 PHPのround関数の謎が少し解けた

 2007-06-05 Pythonのround関数の議論を読んでみた

 2007-06-07 round関数その5:そろそろ反撃していいですか?

 2007-06-11 round関数その6:啓蒙とお詫び

 2007-06-16 round関数その7:偶数丸め

 2007-06-23 round関数その8:RubyとPythonのround関数は奇妙じゃないんですか?

2007-07-03 round関数その9:PerlのMath::Roundモジュールについて

 2007-07-28 PHP以外全員不正解

 2007-08-02 Rubyの浮動小数点数リテラルの扱いは正しいのか

 2008-12-07 PHP5.3.0alpha3のround関数の実装がPHP5.2.6と変わった

 2008-12-09 PHP5.2.7のround関数の実装もPHP5.2.6と変わった

ockeghemockeghem 2007/05/16 11:17 こんにちは。久しぶりにカーニハンの「プログラム書法」に出てくるサンプルを思い出しました。「0.50000000001」・・・確かにステキですね。ソフトウェアの進歩ってなんなのだろうと考えさせられました。

hnwhnw 2007/05/16 12:33 教科書的なことさえ知らないプログラマへの対応に給料取ってる社員の時間を使うのはアホらしいからコードの方を直しちまえ、というような対応なんでしょうかね。ビジネス的には進歩なのかもしれません。

nanashinanashi 2007/05/17 14:05 roundの50.000000001%は優しさでできています。

elfelf 2007/05/18 15:12 warota

pupu 2007/06/02 02:40 5.2.2 でやってみました。ソースは、、みてません。

% uname -mrs
FreeBSD 6.2-RELEASE-p2 i386
% php -v
PHP 5.2.2 (cli) (built: May 30 2007 20:11:02)
Copyright (c) 1997-2007 The PHP Group
Zend Engine v2.2.0, Copyright (c) 1998-2007 Zend Technologies
% php -r ’$x1=0.49999999999;$x2=0.5;var_dump($x1,$x2,($x1===$x2),round($x1),round($x2));’
float(0.49999999999)
float(0.5)
bool(false)
float(0)
float(1)

pupu 2007/06/02 03:01 もうひとつためしてみました。
Mac OS X (10.4.9)

% uname -mrs
Darwin 8.9.1 i386
% php -v
PHP 4.4.4 (cli) (built: Oct 31 2006 23:22:54)
Copyright (c) 1997-2006 The PHP Group
Zend Engine v1.3.0, Copyright (c) 1998-2004 Zend Technologies
% php -r ’$x1=0.49999999999;$x2=0.5;var_dump($x1,$x2,($x1===$x2),round($x1),round($x2));’
float(0.49999999999)
float(0.5)
bool(false)
float(0)
float(1)

hoitohoito 2007/06/02 04:41 なんかphp嫌いが集まりそうなネタだなー
もうちょっと考えよう

hnwhnw 2007/06/02 12:39 puさん:

どうもありがとうございます。バージョンか環境の差かもしれないよ、というご指摘かと思いますけど、バージョンの差ではないと思います。そのあたりのバージョンのソースコードをgrepするとウヨウヨ「0.50000000001」という不思議文字列が出てくるはずです。5.2.3のソースコードもチェックしましたが、特に変わっているようには見えませんでした。

一方で、環境の差というのは考えられます。記事中で「少なくとも僕の手元の環境では」と書いた意図は、PHP_ROUND_FUZZが0.5になっている環境もあるのかもしれない、と考えたからです。この定数がどうやって定義されているかというと、configureスクリプトであるプログラムをテストして、その結果によって切り替えています。

ただ、僕の考えでは、浮動小数点数の四則演算とfloor(3)の挙動が同じなら結果が同じになるプログラムでテストしているように見えたので、余程変わったアーキテクチャ上でない限り僕の環境と同じ結果になるはずだ、と考えていました。

ただ、0.5になっている環境もあるということですから、僕が何か考え落としている点があるようです。もう少し実験してみたいと思います。

まずは結果をお送りいただいてありがとうございます。赤の他人の言うことを鵜呑みにしない姿勢に感心するとともに感謝いたします。

hnwhnw 2007/06/02 14:31 CFLAGS=”-O0” ./configure したらPHPがマトモに生まれ変わりました。僕の手元のgcc 3.4.6の-O2に関する最適化のバグかもしれません。PHPの意図は相変わらず謎ですけど、もうすこし調べてから別エントリにまとめてみます。

H.HolonH.Holon 2007/06/03 04:07 PHP_ROUND_FUZZの値は、configure scriptの中で設定しているようです。
そこで環境の生での演算精度をみて自前を使うか判断していて、それが bugtrackであがってる場合だけ見ているようなので、予想外の環境だとコケるようですね。

hnwhnw 2007/06/03 15:21 ありがとうございます。環境の差について最新の記事にまとめてみました。はてな初心者なので、どう誘導すればいいのか悩んでしまいますが、続きに興味がある方は http://d.hatena.ne.jp/hnw/20070603 をご覧ください。

はてブへに対するお返事もしてみます。はてブの人にはブックマーク上でお返事するのがいいんですかね?

Bookmarkerさん:『round(3)はC99で追加された関数なので、使わないのは不思議ではない。』
確かにそうですね。ANSI Cに含まれていたような気がしたのですが、勘違いでした。Perlにroundが無い時点で気づくべきでした。

moriyoshimoriyoshi 2007/06/03 23:43 cvs.php.net で annotate してみると、問題箇所の修正のきっかけになったバグは bug #25694 でしたよ。

http://bugs.php.net/25694

hnwhnw 2007/06/04 01:50 ありがとうございます。パッと見では#24142でroundの挙動を変更してこの#25694でnumber_formatを追随させたように見えるんですが、どうなんでしょうか。そういえば僕があのバグを引っ張ってきた理由を書いていませんでしたが、ソースコード中のPHP_ROUND_FUZZの近くに/* see #24142 */なんて書いてあったためです。

 | 
ページビュー
1815947