Hatena::ブログ(Diary)

餡子付゛録

2011-04-09

PHP 5.3のround関数の挙動は仕様かも

PHPは4.06までしか知らないJava屋からPHPerのid:hnw氏へ。「PHPの新しいround関数にバグをみつけた」に関して、ちょっと疑問があります。

Bogus扱いにされて怒っているPHP :: Bug #54479ですが、症状は確認できました。

しかし、バグでは無さそうです。まずは以下のコードを見てください。

問題が出るケース

以下のhnw_float_01.phpを書いてみます。有効桁数は17。

ini_set("precision",17);

var_dump(12.345);

var_dump(round(12.345, 1));

実行してみます。おお、確かに問題が出ます。アニソン歌えそうです。

$ php hnw_float_01.php

float(12.345000000000001)

float(12.300000000000001)

仮数をギリギリの大きさにしなくても、同じ症状が発生するところがポイントです。

問題が出ないケース

以下のhnw_float_02.phpを書いてみます。有効桁数は16。

ini_set("precision",16);

var_dump(12.345);

var_dump(round(12.345, 1));

あれ、丸め処理される? ウィンクを歌うおじさんの声が聞こえてきました。

$ php hnw_float_02.php

float(12.345)

float(12.3)

理由はたぶん単純、倍精度の桁数は10進数で16桁だから

英語版のwikipediaに、仮数は53 bits(sをつけるか-BITにしてね)で、10進数に直すとだいたい16桁って書いてある。

its significand has a precision of 53 bits (about 16 decimal digits).

デフォルトはini_set("precision",14);なので、仕様の問題は17以上を指定したときの挙動だと思います。

PHPの挙動を変更するとすれば、値が17以上は警告を出して、かつ自動的に16に再設定するのが適切かと。

間違っていたらごめんなさい。PHPは4.06までしか良く分からないんです。

PHP 5.2.17で問題が出ない理由

PHP 5.2.xまでは、80bitsで計算していたら、19桁でもいけたのでしょうね。

浮動小数演算PHPでしちゃいけないと、ばーちゃんの友達の隣人の他人が言っていたのを夢で見たので、詳しい事は分かりません。後はPHPerの皆様、よろしくお願いします。

その他

PHP 5.3.2-1ubuntu4.7で確認しました。

浮動小数演算を使うなら、こういうモノに使いたいです。

hnwhnw 2011/04/10 10:36 僕がバグだと主張している内容について、誤解されているような気がします。せっかく作って頂いたサンプルコードですが、状況が全然違いますよ。あなたもPHPの中の人も、脊髄反射で「あ、よくある話か」って考えたんだろうと思うんですがね…。

僕が「問題が出る」と言っているケースは、round関数が引数をそのまま返すような場合です。作って頂いたサンプルコードは小数点以下第2位第3位がしっかり変わっていまし、その結果について何も疑問点はありません。

また、サンプルコードの結果はPHP5.2.17でも32bit環境でも64bit環境でも同じです。

uncorrelateduncorrelated 2011/04/10 15:52 コメントありがとうございます。なるほど、症状が違うわけですね。端数が残っているので、同じだと誤解していました。
ところでIEEE754の倍精度では、10進数の19桁は桁あふれでサポートされない気がするのですが、その点はid:hnwさんは検証されたのでしょうか?

ini_set("precision",19);

ここが17以上になっていると、IEEE754を超えているので何が起きても仕方が無いと思われるのではないでしょうか?
16以下で問題を引き起こすサンプルを提示すれば、バグとして認識してもらえるのでは無いでしょうか?

hnwhnw 2011/04/10 19:08 浮動小数点数の精度に関して誤解があると思います。precisionを16以下にしないと無意味だ、という認識が誤りです。PHPの中の人も似たような認識違いをしているようなので、どう返答したものか困っているところです。

2進と10進は違うので、10進小数の常識でdoubleの精度を捉えているとほころびが出ることがあります。doubleの精度が10進換算で約16桁という記述から、10進で16桁になると1刻みでしか表現できない、17桁以上にしても無意味だ、とお考えのように思いますが、それは不正確です。実際には10^(15.9)=9000兆くらいで1刻みになり、それより小さい範囲では0.5刻みや0.25刻みや0.125刻みで表現できるのです。

たとえば4000兆付近ではdoubleは0.5刻みになります。ざっくりlog10(2)=0.3、log10(4)=0.6と考えると、最上位の桁(4)には10進換算で0.6桁の精度があり、小数点以下1位(0.5刻み)は10進0.3桁の精度があるので、全体で15.9桁の精度があるという風に捉えることができます。この場合、15.9桁の精度しか無いにもかかわらず、10進で17桁表現する意味があると思うのですが、いかがでしょうか。

また、今回議論しているのはroundについてなので、小数点以下第一位を0.1刻みで表現できる必要性はありません。0.5より大きいか小さいかが表現できるなら、丸め処理には意味があります。

ですから、僕のバグレポートのprecisionが17なのは意味があります。ただ、0.1という数がdoubleでピッタリ表現できないことから誤解を生んだかと考えて、1000兆付近では0.125刻みであることを明確にするためにさらに精度を上げたところ、浮動小数点数に関して10進感覚で捉えているような返答が返ってきて頭を抱えているというわけです。

正直なところ、このレベルの話だと10進のメタファーで話し続けること自体が無意味に思えます。2^52と、そこから-0.5および-1.0した数について、どんなビットパターンになるかをJavaで書いてみましたので、少し遊んでみてください。いずれも10進で16桁の数ですが、それぞれ別の数であることはわかるはずです。

public class Double2BitPattern {
public static void main(String[] args) {
double d1 = 4503599627370496.0;
double d2 = d1-0.5;
double d3 = d1-1.0;
long bitPattern1 = Double.doubleToLongBits(d1);
long bitPattern2 = Double.doubleToLongBits(d2);
long bitPattern3 = Double.doubleToLongBits(d3);
System.out.println("d1: " + Long.toHexString(bitPattern1));
System.out.println("d2: " + Long.toHexString(bitPattern2));
System.out.println("d3: " + Long.toHexString(bitPattern3));
}
}

hnwhnw 2011/04/10 19:20 バグレポを読み返してみたら初回は18桁にしてましたね…。17桁でもバグだという主張は変わらないので、その意味では中途半端だったかもしれません。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

おとなり日記