Rubyのround関数の実装が変わってた
今週は国際浮動小数点数ウイークですので、週の最後もやはり浮動小数点数ネタです。
Rubyのround関数の実装に関して、1.8.6 patchlevel 114と1.8.6 patchlevel 230の間に変更が入っています。1.8.6-p230のリリース時期は2008/06/20のようですから、1.8.7(2008/06/01リリース)から変更されたと言っていいかもしれません。
これらは、僅かながら挙動が違っています。MacOSX環境で実験しました。(再現できるのはFreeBSDとMacOSXだけのような気がします)
$ /usr/bin/ruby --version ruby 1.8.6 (2008-03-03 patchlevel 114) [universal-darwin9.0] $ /usr/bin/ruby -e 'p 9007199254740991.0.round;' 9007199254740992 $ /opt/local/bin/ruby --version ruby 1.8.7 (2008-08-11 patchlevel 72) [i686-darwin9] $ /opt/local/bin/ruby -e 'p 9007199254740991.0.round;' 9007199254740991
なんと、同じ数字の丸め結果が違う数字になりました。中身を簡単に説明すると、前者は自前実装していたのが、後者はlibmのroundに丸投げしています。roundが無い環境のために自前のroundも用意してありますが、これも1.8.6-p114以前とは異なる実装です。
1.8.6-p114の方が不思議な挙動に見えますので、修正自体は良いと思います。とはいえ、PHP以外の言語でも、マイナーバージョンアップで基本的な関数の実装をいじることがあるんですね。その点は少しビックリしました。
実装の違い
では、Cソースコードを見てみましょう。まずは1.8.6-p114のソースコードから。
static VALUE flo_round(num) VALUE num; { double f = RFLOAT(num)->value; long val; if (f > 0.0) f = floor(f+0.5); if (f < 0.0) f = ceil(f-0.5); if (!FIXABLE(f)) { return rb_dbl2big(f); } val = f; return LONG2FIX(val); }
この通り、自前で実装しています。1.8.7からは下記のようになりました。
#ifndef HAVE_ROUND double round(x) double x; { double f; if (x > 0.0) { f = floor(x); x = f + (x - f >= 0.5); } else if (x < 0.0) { f = ceil(x); x = f - (f - x >= 0.5); } return x; } #endif
static VALUE flo_round(num) VALUE num; { double f = RFLOAT(num)->value; long val; f = round(f); if (!FIXABLE(f)) { return rb_dbl2big(f); } val = f; return LONG2FIX(val); }
1.8.7では、round関数が無い環境のみ自前実装を使うようになっています。実際のところ、roundが無い環境はWindowsくらいでしょうか。自前実装についても以前とは微妙に計算順序が変わっており、環境次第では異なる結果になる可能性があるように思います。
背景とか
この変更が入った原因は [ruby-dev:32247] あたりの議論にあるようですね。「なあに、かえって精度が増す」みたいな話も出ていますけど、後方互換性は気にしなくていいのか、人ごとながら気になりました。
また、ライブラリに丸投げしてしまうと、環境によって挙動の差が出ることがあります。これを嫌って、PHPではライブラリコールしていたのを自前実装に切り替えたりしている例もあります。その意味でも、自前実装をライブラリコールに切り替えて大丈夫なのかな、という気がします。
最後に
そんな変な週はおそらく無いと思います。