Monoのround関数にバグを見つけたよ、という毎度おなじみの話題です。
早速ですが、浮動小数点数で扱える一番大きい数を浮動小数点数以下第2位で丸めて第1位までにしてみましょう。
using System; class RoundingBigFloat { static void Main() { double d = Double.MaxValue; Console.WriteLine(d); // 1.79769313486232E+308 Console.WriteLine(Math.Round(d, 1)); // Infinity } }
上記コードのコメント部の通り、大きい数を丸めると無限大になることがあります。これは、小数点以下第n位までに丸める処理が、「10^n倍して整数に丸めて10^-n倍する」という処理になっているため、この計算の途中で無限大になってしまうことがあるのです。
Monoのソースコードも見てみた
対応するMonoの処理を追ってみましょう。MonoのC#コンパイラmcsはC#で書かれています。
浮動小数点数のround関数の実装はclass/corlib/System/Math.csに見つかります。
public static double Round (double value, int digits) { if (digits < 0 || digits > 15) throw new ArgumentOutOfRangeException (Locale.GetText ("Value is too small or too big.")); if (digits == 0) return Round (value); return Round2(value, digits, false); } [MethodImplAttribute (MethodImplOptions.InternalCall)] private extern static double Round2 (double value, int digits, bool away_from_zero);
Round関数の第2引数が指定された場合はRound2関数が呼ばれることがわかります。Round2関数の属性に「InternalCall」と書いてあるので、CLI側で定義されている関数呼び出しの意味なのかなと想像できます。
実際、Round2関数の実体はMono側に見つかります。以下はmono/metadata/sysmath.cからの抜粋です。
gdouble ves_icall_System_Math_Round2 (gdouble value, gint32 digits, gboolean away_from_zero) { #if !defined (HAVE_ROUND) || !defined (HAVE_RINT) double int_part, dec_part; #endif double p; MONO_ARCH_SAVE_REGS; if (value == HUGE_VAL) return HUGE_VAL; if (value == -HUGE_VAL) return -HUGE_VAL; p = pow(10, digits); #if defined (HAVE_ROUND) && defined (HAVE_RINT) if (away_from_zero) return round (value * p) / p; else return rint (value * p) / p; #else dec_part = modf (value, &int_part); dec_part *= 1000000000000000ULL; if (away_from_zero && dec_part > 0) dec_part = ceil (dec_part); else dec_part = floor (dec_part); dec_part /= (1000000000000000ULL / p); if (away_from_zero) { if (dec_part > 0) dec_part = floor (dec_part + 0.5); else dec_part = ceil (dec_part - 0.5); } else dec_part = ves_icall_System_Math_Round (dec_part); dec_part /= p; return ves_icall_System_Math_Round ((int_part + dec_part) * p) / p; #endif }
すでに説明したとおり、受け取った浮動小数点数を10^n倍している箇所が見つかります。
roundかrintのどちらかが無い環境むけのコード(#else部)から怪しい気配がプンプンしますが、こちらの分岐を通る環境は珍しいと思うので、見なかったことにします。