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

Rob*_*Rob 5 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;
}
Run Code Online (Sandbox Code Playgroud)

结果

3.59538626972463E+307
Infinity
Run Code Online (Sandbox Code Playgroud)

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

phu*_*clv 1

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

\n
3,59538626972463E+307\n\xe2\x88\x9e\n
Run Code Online (Sandbox Code Playgroud)\n

注意:小数点和无穷大表示取决于区域设置

\n

32位Debug模式下结果是这样的

\n
3,59538626972463E+307\n3,59538626972463E+307\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
\xe2\x88\x9e\n\xe2\x88\x9e\n
Run Code Online (Sandbox Code Playgroud)\n

演示:

\n\n

这是因为在 32 位 .NET Standard 中,浮点运算是使用80位扩展浮点格式的x87 指令完成的。不适合 a并导致无穷大,但在扩展精度中它不会溢出并且最终结果适合 anumSteps * stepdoubledouble

\n

但为什么有时是无穷大3.59538626972463E+307,有时又是无穷大呢?这是因为在计算表达式时,有时必须将中间值溢出到内存中以释放一些寄存器,从而将值降低为double精度。因此,相同的表达式可能会产生不同的结果。我们不知道编译器何时溢出到内存,因此我们无法预测输出。影响结果的另一件事是编译器优化:这里GetVal2是由编译器预先计算的,它会double精确地完成所有操作。你可以很容易地看到编译器只是从内存中加载常量结果,而不是在\xc2\xa7GetVal2下面的反汇编中调用

\n

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

\n

为了避免这种非确定性功能,最简单的方法是完全避免 32 位 x86,或者迁移到更能保证确定性的 .NET Core。否则你将不得不使用一些第三方。看

\n\n
\n

类似的问题还有很多:

\n\n

在 C 和 C++ 中,这也是同样的问题FLT_EVAL_METHOD > 1.

\n\n
\n

\xc2\xa7这是完整的反汇编列表和我的评论。许多行消失了,因为它们已被优化掉

\n
\n命名空间 FloatDeterminism\n{\n 类 Program\n {\n static void Main(string[] args)\n {\n var x = Double.MinValue;\n02C40848 55 Push ebp\n02C40849 8B EC mov ebp,esp\ n 02C4084B DD 05 90 08 C4 02 fld qword ptr [FloatDeterminism.Program.Main(System.String[])+048h (02C40890h)] # 加载 -1.7976931348623157e+308 \n02C40851 83 EC 08 sub esp,8\n02C40第854章 1C 24 fstp qword ptr [esp]\n 02C40857 DD 05 98 08 C4 02 fld qword ptr [FloatDeterminism.Program.Main(System.String[])+050h (02C40898h)] # 加载 3.5953862697246315e+307 \n02C4085D 83 EC 08子esp,8\n02C40860 DD 1C 24 fstp qword ptr [esp]\n02C40863 B9 06 00 00 00 mov ecx,6\n 02C40868 FF 15 60 4D 1C 01 调用 dword ptr [指向: FloatDeterminism.Program.GetVal(Double, Int) 32 , Double) (011C4D60h)] # Call GetVal() \n02C4086E 83 EC 08 sub esp,8\n02C40871 DD 1C 24 fstp qword ptr [esp]\n 02C40874 E8 7F 10 2F 70 call System.Console.WriteLine(Double) ( 72F318F8h) # 打印 GetVal() \n 02C40879 DD 05 A0 08 C4 02 fld qword ptr [FloatDeterminism.Program.Main(System.String[])+058h (02C408A0h)] # 加载 inf \n02C4087F 83 EC 08 sub esp,8 \n02C40882 DD 1C 24 fstp qword ptr [esp]\n02C40885 E8 6E 10 2F 70       call        System.Console.WriteLine(Double) (72F318F8h)  # Print GetVal2() = inf\n02C4088A 5D                   pop         ebp\n02C4088B C3                   ret\n            Console.WriteLine(GetVal2(x, 6, diff));\n02C4088C 00 00                add         byte ptr [eax],al\n02C4088E 00 00                add         byte ptr [eax],al\n02C40890 FF                   ?? ??????\n02C40891 FF                   ?? ??????\n02C40892 FF                   ?? ??????\n02C40893 FF                   ?? ??????\n02C40894 FF                   ?? ??????\n02C40895 FF                   ?? ??????\n            Console.WriteLine(GetVal2(x, 6, diff));\n02C40896 EF                   out         dx,eax\n02C40897 FF 99 99 99 99 99    call        fword ptr [ecx-66666667h]\n02C4089D 99                   cdq\n02C4089E C9                   leave\n02C4089F 7F 00                jg          FloatDeterminism.Program.Main(System.String[])+059h (02C408A1h)\n02C408A1 00 00                add         byte ptr [eax],al\n02C408A3 00 00                add         byte ptr [eax],al\n02C408A5 00 F0                add         al,dh\n02C408A7 7F 20                jg          FloatDeterminism.Program.GetVal(Double, Int32, Double)+011h (02C408C9h)\n02C408A9 13 1C 01             adc         ebx,dword ptr [ecx+eax]\n02C408AC 00 00                add         byte ptr [eax],al\n02C408AE 00 00                add         byte ptr [eax],al\n02C408B0 14 13                adc         al,13h\n02C408B2 1C 01                sbb         al,1\n02C408B4 58                   pop         eax\n02C408B5 4D                   dec         ebp\n02C408B6 1C 01                sbb         al,1\n\n        public static double GetVal(double start, int numSteps, double step)\n        {\n            var res = start + numSteps * step;\n02C408B8 56                   push        esi\n02C408B9 50                   push        eax\n02C408BA 89 0C 24             mov         dword ptr [esp],ecx\n02C408BD DB 04 24             fild        dword ptr [esp]\n02C408C0 DC 4C 24 0C          fmul        qword ptr [esp+0Ch]\n02C408C4 DC 44 24 14          fadd        qword ptr [esp+14h]\n02C408C8 DD 05 00 09 C4 02    fld         qword ptr [FloatDeterminism.Program.GetVal(Double, Int32, Double)+048h (02C40900h)]\n02C408CE DF F1                fcomip      st,st(1)  # Check for NaN\n02C408D0 7A 06 jp FloatDeterminism.Program.GetVal(Double、Int32、Double)+020h (02C408D8h)\n02C408D2 75 04 jne FloatDeterminism.Program.GetVal(Double、Int32、Double)+020h (02C408D8h)\n02C4 08D4 DD D8 fstp st (0)\n02C408D6 EB 05 jmp FloatDeterminism.Program.GetVal(Double, Int32, Double)+025h (02C408DDh)\n02C408D8 59 pop ecx\n02C408D9 5E pop esi\n02C408DA C2 10 00 ret 10h\n02C40 8DD B9 74 D6 3F 72 mov ecx,723FD674h\n02C408E2 E8 0D 28 57 FE 调用 CORINFO_HELP_NEWSFAST (011B30F4h)\n02C408E7 8B F0 mov esi,eax\n02C408E9 8B CE mov ecx,esi\n 抛出新的 InvalidOperationException();\n02C40 8EB FF 15 AC D6 3F 72 调用双字指针[指向:System.InvalidOperationException..ctor() (723FD6ACh)]\n02C408F1 8B CE mov ecx,esi\n02C408F3 E8 88 2D 9D 71 call 74613680\n02C408F8 CC int 3\n02C408F9 CC int 3\n
\n


归档时间:

查看次数:

110 次

最近记录:

8 年,11 月 前