hnwの日記

Monoで巨大な浮動小数点数を丸めたら無限大になった

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部)から怪しい気配がプンプンしますが、こちらの分岐を通る環境は珍しいと思うので、見なかったことにします。