hnwの日記

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

題名の通りなんですが、前回の記事「PHP以外全員不正解」に対して「ダウト!」を頂戴したのでまとめてみます。

Cのこの動作が、唯一無二絶対のものであるとする根拠はどこにあるのでしょうか? strtod によれば、

If the subject sequence has the decimal form and at most DECIMAL_DIG (defined in ) significant digits, the result should be correctly rounded. If the subject sequence D has the decimal form and more than DECIMAL_DIG significant digits, consider the two bounding, adjacent decimal strings L and U, both having DECIMAL_DIG significant digits, such that the values of L, D, and U satisfy L <= D <= U. The result should be one of the (equal or adjacent) values that would be obtained by correctly rounding L and U according to the current rounding direction, with the extra stipulation that the error with respect to D should have a correct sign for the current rounding direction.

DEICMAL_DIGというのはC99で追加された定数で、glibcを使ったシステムだと多分17です。 Sparc上のシステムでは20とかいうのもあるみたいですがまあそれはさておき、この数を越えた桁数を持つ数値に関しては、 should be one of the (equal or adjacent) values that would be obtained by correctly rounding L and U according to the current rounding direction とはありますが、最近値丸めにしなければならないとはどこにも書いていません。ですから先の例にあるように DECIMAL_DIGを越えた桁数を文字列化したときに最近値丸めでないからといって即バグであるという判断を下すことはできないと思います。

ときどきの雑記帖リターンズ - では異議を唱えてみよう

なるほど、こんな仕様があるとは知りませんでした。ありがとうございます。では、各言語がこの仕様を満たしている可能性があるのかどうか、調べてみましょう。

Rubyのmissing/strtod.cを読んだところ、浮動小数点数の10進表記の19桁目から先は0とみなして18桁までを処理していると理解しました。また、Perlは18桁目で偶数丸め*1して10進17桁としているようです*2

さて、これらはC99のDECIMAL_DIGがいくつの場合に相当するのでしょうか。「If the subject sequence has the decimal form and at most DECIMAL_DIG (defined in ) significant digits, the result should be correctly rounded.」ということですから、実際の結果と照らし合わせて考えてみましょう。

$ perl -e 'printf("%21.0f\n",36893488147419108000);'
 36893488147419103232

10進で有効数字17桁の数を64bit浮動小数点数に丸めてみました。この数をto nearestで丸めたとしたら36893488147419111424の方が近いですから、正しく丸められていません。つまり、DECIMAL_DIGで換算すると16以下ということになります。

ではRubyはどうでしょうか。前回記事の環境とは異なるのですが、問題の出る環境を探してみました。

$ uname -mrs
FreeBSD 6.1-RELEASE-p10 i386
$ ruby -v
ruby 1.8.5 (2006-08-25) [i386-freebsd6]
$ ruby -e 'printf("%21.0f\n",36893488147419108000.0);'
 36893488147419103232

この環境ではPerlと同じ結果になりましたので、DECIMAL_DIGで言えばやはり16以下だということがわかります。

調べ始めた時点からすると予想外なのですが、これはやはり実装がまずいと言えそうです。バグと言うとムッとされる方がいるかもしれませんが、もっと上手い実装があるのは確かでしょう。

このような結果となる原因はPerlRubyも共通で、巨大な浮動小数点数リテラルを処理するのに(内部的な有効桁数分の整数)×10nとして計算しているためです。「内部的な有効桁数分の整数」が計算途中で一度IEEE64bit浮動小数点数として保存されてしまうと仮数部の53bitでは10進17桁や18桁はカバーできず、この時点で丸められてしまうのです。丸められた結果を後から10n倍しても、本来の数を丸めた結果とは異なってしまいます。

最後に念のため書いておくと、「やったー!珍しくPHPが勝ったよ!ヒャッホー!でもなぜか虚しい…」という程度のネタですので、他の言語の方々はあまり深刻に受け取らないで欲しいです*3PHPのダメなところなんて腐るほどあるわけですから、たまには勝たせてあげてください。


追記:LL魂で晒されました。いきなり自分の日記のURLが出てきてビビりましたよ!>ますがたさん


追記2:本件がruby1.9に取り込まれる予定だそうで。(via http://d.hatena.ne.jp/masugata/20070822#p2)

自分としては誰も興味を持たないくらい細かい内容だと思っていたので、ちゃんと修正しちゃうRubyコミュニティは凄いと感じます。PHPの人も皆もっとCソースコードを読もうよ、って呼びかけていきたいですね。

*1:怪しいですがソースコードにそう書いてあります

*2:少なくとも私の環境ではそうです

*3:この手の話が大好きな方なら止めませんけど