不相关的代码会更改计算结果

时间:2017-10-02 13:54:13

标签: c# floating-point cpu-registers

我们有一些代码会在某些机器上产生意外结果。我把它缩小到一个简单的例子。在下面的linqpad片段中,方法GetValGetVal2具有基本相同的实现,但前者还包括对NaN的检查。但是,每个返回的结果都不同(至少在我的机器上)。

void Main()
{
    var x = Double.MinValue;
    var y = Double.MaxValue;
    var diff = y/10 - x/10;

    Console.WriteLine(GetVal(x,6,diff));
    Console.WriteLine(GetVal2(x,6,diff));
}

public static double GetVal(double start, int numSteps, double step)
{
    var res = start + numSteps * step;
    if (res == Double.NaN)
        throw new InvalidOperationException();
    return res;
}

public static double GetVal2(double start, int numSteps, double step)
{
    return start + numSteps * step;
}

结果

3.59538626972463E+307
Infinity

为什么会发生这种情况,是否有一种避免它的简单方法?与寄存器有关吗?

1 个答案:

答案 0 :(得分:1)

您没有指定环境和编译选项,但我已经能够在 .NET Standard 4.8 中以 32 位发布模式重现您的问题,其中输出是

3,59538626972463E+307
∞

32位调试模式下结果是这样的

3,59538626972463E+307
3,59538626972463E+307

在 64 位模式下,结果始终如下

∞
∞

这是因为在 32 位 .NET Standard 中,浮点运算是使用 80 位扩展浮点格式的 x87 指令完成的。 numSteps * step 不适合 double 并导致无穷大,但在扩展精度中它不会溢出并且最终结果适合 double。在计算表达式时,有时必须将中间值溢出到内存以释放一些寄存器,这会将值转换为 double 精度。因此,相同的表达式可能会产生不同的结果。影响结果的另一件事是编译器优化:这里的 GetVal2 是由编译器预先计算的,它以 double 精度执行所有操作。你可以很容易地看到编译器只是从内存中加载常量结果,而不是在下面的反汇编中调用 GetVal2§

这种现象在 64 位模式下永远不会发生,无论是 Debug 还是 Release 以及 .NET Standard 或 .NET Core,因为 64 位中的数学是总是在 SSE 寄存器中完成。它也无法在 .NET Core 中重现,因为 .NET Core 也使用 SSE 寄存器来进行浮点数学运算,即使在 32 位中也是如此。您可以通过在 VS 中调试并查看反汇编来轻松检查。这是有道理的,因为 SSE 速度更快,结果更具确定性,而且过去 20 年中几乎所有现代计算机都支持 SSE

要避免这种非确定性功能,最简单的方法是完全避免使用 32 位 x86,或者转移到 .NET Core,在那里确定性更有保证。否则,您将不得不使用一些 3rd 方库。见


有很多类似的问题:

在 C 和 C++ 中使用 FLT_EVAL_METHOD > 1 时存在相同的问题。


§ 这是带有我的评论的完整反汇编列表。许多行消失了,因为它们被优化了

namespace FloatDeterminism
{
    class Program
    {
        static void Main(string[] args)
        {
            var x = Double.MinValue;
02C40848 55                   push        ebp
02C40849 8B EC                mov         ebp,esp
02C4084B DD 05 90 08 C4 02    fld         qword ptr [FloatDeterminism.Program.Main(System.String[])+048h (02C40890h)]  # Load -1.7976931348623157e+308
02C40851 83 EC 08             sub         esp,8
02C40854 DD 1C 24             fstp        qword ptr [esp]
02C40857 DD 05 98 08 C4 02    fld         qword ptr [FloatDeterminism.Program.Main(System.String[])+050h (02C40898h)]  # Load 3.5953862697246315e+307
02C4085D 83 EC 08             sub         esp,8
02C40860 DD 1C 24             fstp        qword ptr [esp]
02C40863 B9 06 00 00 00       mov         ecx,6
02C40868 FF 15 60 4D 1C 01    call        dword ptr [Pointer to: FloatDeterminism.Program.GetVal(Double, Int32, Double) (011C4D60h)]  # Call GetVal()
02C4086E 83 EC 08             sub         esp,8
02C40871 DD 1C 24             fstp        qword ptr [esp]
02C40874 E8 7F 10 2F 70       call        System.Console.WriteLine(Double) (72F318F8h)  # Print GetVal()
02C40879 DD 05 A0 08 C4 02    fld         qword ptr [FloatDeterminism.Program.Main(System.String[])+058h (02C408A0h)]  # Load inf
02C4087F 83 EC 08             sub         esp,8
02C40882 DD 1C 24             fstp        qword ptr [esp]
02C40885 E8 6E 10 2F 70       call        System.Console.WriteLine(Double) (72F318F8h)  # Print GetVal2() = inf
02C4088A 5D                   pop         ebp
02C4088B C3                   ret
            Console.WriteLine(GetVal2(x, 6, diff));
02C4088C 00 00                add         byte ptr [eax],al
02C4088E 00 00                add         byte ptr [eax],al
02C40890 FF                   ?? ??????
02C40891 FF                   ?? ??????
02C40892 FF                   ?? ??????
02C40893 FF                   ?? ??????
02C40894 FF                   ?? ??????
02C40895 FF                   ?? ??????
            Console.WriteLine(GetVal2(x, 6, diff));
02C40896 EF                   out         dx,eax
02C40897 FF 99 99 99 99 99    call        fword ptr [ecx-66666667h]
02C4089D 99                   cdq
02C4089E C9                   leave
02C4089F 7F 00                jg          FloatDeterminism.Program.Main(System.String[])+059h (02C408A1h)
02C408A1 00 00                add         byte ptr [eax],al
02C408A3 00 00                add         byte ptr [eax],al
02C408A5 00 F0                add         al,dh
02C408A7 7F 20                jg          FloatDeterminism.Program.GetVal(Double, Int32, Double)+011h (02C408C9h)
02C408A9 13 1C 01             adc         ebx,dword ptr [ecx+eax]
02C408AC 00 00                add         byte ptr [eax],al
02C408AE 00 00                add         byte ptr [eax],al
02C408B0 14 13                adc         al,13h
02C408B2 1C 01                sbb         al,1
02C408B4 58                   pop         eax
02C408B5 4D                   dec         ebp
02C408B6 1C 01                sbb         al,1

        public static double GetVal(double start, int numSteps, double step)
        {
            var res = start + numSteps * step;
02C408B8 56                   push        esi
02C408B9 50                   push        eax
02C408BA 89 0C 24             mov         dword ptr [esp],ecx
02C408BD DB 04 24             fild        dword ptr [esp]
02C408C0 DC 4C 24 0C          fmul        qword ptr [esp+0Ch]
02C408C4 DC 44 24 14          fadd        qword ptr [esp+14h]
02C408C8 DD 05 00 09 C4 02    fld         qword ptr [FloatDeterminism.Program.GetVal(Double, Int32, Double)+048h (02C40900h)]
02C408CE DF F1                fcomip      st,st(1)  # Check for NaN
02C408D0 7A 06                jp          FloatDeterminism.Program.GetVal(Double, Int32, Double)+020h (02C408D8h)
02C408D2 75 04                jne         FloatDeterminism.Program.GetVal(Double, Int32, Double)+020h (02C408D8h)
02C408D4 DD D8                fstp        st(0)
02C408D6 EB 05                jmp         FloatDeterminism.Program.GetVal(Double, Int32, Double)+025h (02C408DDh)
02C408D8 59                   pop         ecx
02C408D9 5E                   pop         esi
02C408DA C2 10 00             ret         10h
02C408DD B9 74 D6 3F 72       mov         ecx,723FD674h
02C408E2 E8 0D 28 57 FE       call        CORINFO_HELP_NEWSFAST (011B30F4h)
02C408E7 8B F0                mov         esi,eax
02C408E9 8B CE                mov         ecx,esi
                throw new InvalidOperationException();
02C408EB FF 15 AC D6 3F 72    call        dword ptr [Pointer to: System.InvalidOperationException..ctor() (723FD6ACh)]
02C408F1 8B CE                mov         ecx,esi
02C408F3 E8 88 2D 9D 71       call        74613680
02C408F8 CC                   int         3
02C408F9 CC                   int         3