.net core 3 产生与 2.2 版不同的浮点结果

.net core 3 yields different floating point results from version 2.2

这是一段示例代码,其中包含 .net core 2.2 和 3.1 的输出。它显示了基本浮点表达式 a^b 的不同计算结果。

在此示例中,我们计算 1.9 的 3 次方。以前的 .NET 框架产生了正确的结果,但 .net core 3.0 和 3.1 产生了不同的结果。

这是有意更改吗?我们如何才能将财务计算代码迁移到新版本并保证数值计算仍会产生相同的结果? (如果 .NET 也有十进制数学库就好了)。

    public static class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("--- Decimal ---------");
            ComputeWithDecimalType();
            Console.WriteLine("--- Double ----------");
            ComputeWithDoubleType();

            Console.ReadLine();
        }

        private static void ComputeWithDecimalType()
        {
            decimal a = 1.9M;
            decimal b = 3M;
            decimal c = a * a * a;
            decimal d = (decimal) Math.Pow((double) a, (double) b);

            Console.WriteLine($"a * a * a                        = {c}");
            Console.WriteLine($"Math.Pow((double) a, (double) b) = {d}");
        }

        private static void ComputeWithDoubleType()
        {
            double a = 1.9;
            double b = 3;
            double c = a * a * a;
            double d = Math.Pow(a, b);

            Console.WriteLine($"a * a * a      = {c}");
            Console.WriteLine($"Math.Pow(a, b) = {d}");
        }
    }

.NET 核心 2.2

--- 十进制 ----------

a * a * a                        = 6.859
Math.Pow((double) a, (double) b) = 6.859

--- 双倍 ----------

a * a * a      = 6.859
Math.Pow(a, b) = 6.859

.NET 核心 3.1

--- 十进制 ----------

a * a * a                        = 6.859
Math.Pow((double) a, (double) b) = 6.859

--- 双倍 ----------

a * a * a      = 6.858999999999999
Math.Pow(a, b) = 6.858999999999999

.NET Core 在 IEEE 浮点合规性方面引入了很多 floating point parsing and formatting improvements。其中之一是 IEEE 754-2008 格式合规性。

在 .NET Core 3.0 之前,ToString() 内部将精度限制为 "just" 15 个位置,生成无法解析回原始字符串的字符串。问题的值相差 一位

在 .NET 4.7 和 .NET Core 3 中,实际字节数保持不变。在这两种情况下,调用

BitConverter.GetBytes(d*d*d)

产生

85, 14, 45, 178, 157, 111, 27, 64

另一方面,BitConverter.GetBytes(6.859) 产生:

86, 14, 45, 178, 157, 111, 27, 64

即使在 .NET Core 3 中,解析“6.859”也会生成第二个字节序列:

BitConverter.GetBytes(double.Parse("6.859"))

这是一个位的差异。旧行为产生了一个无法解析回原始值的字符串

此更改解释了差异:

ToString(), ToString("G"), and ToString("R") will now return the shortest roundtrippable string. This ensures that users end up with something that just works by default.

这就是为什么我们在处理浮点数时总是需要指定一个精度。这种情况也有改进:

For the "G" format specifier that takes a precision (e.g. G3), the precision specifier is now always respected. For double with precisions less than 15 (inclusive) and for float with precisions less than 6 (inclusive) this means you get the same string as before. For precisions greater than that, you will get up to that many significant digits

使用 ToString("G15") 产生 6.859ToString("G16") 产生 6.858999999999999,它有 16 个小数位。

这提醒我们在处理浮点数时始终需要指定精度,无论是比较还是格式化