调试/释放模式下的浮点/双精度

Float/double precision in debug/release modes

提问人:MichaelT 提问时间:9/18/2008 最后编辑:Daniel FischerMichaelT 更新时间:3/28/2019 访问量:7302

问:

调试模式和发布模式之间的 C#/.NET 浮点运算的精度是否不同?

C# .NET 浮点

评论

0赞 Mats Fredriksson 9/18/2008
你认为它们为什么不同?
0赞 J D OConal 9/18/2008
是的,我也有兴趣了解你的思维过程。
0赞 Skizz 9/18/2008
问题是关于调试和发布之间的区别。您可能会认为发布版本使用寄存器而不是 RAM,这将是更高的精度:FPU = 80 位,double=64 位,float = 32 位。
0赞 MichaelT 9/18/2008
谢谢大家,我发现了几篇文章,其中说浮点数的行为在发布模式下会有所不同 blogs.msdn.com/davidnotario/archive/2005/08/08/449092.aspx

答:

2赞 Dark Shikari 9/18/2008 #1

事实上,如果调试模式使用 x87 FPU,而发布模式使用 SSE 进行浮点运算,则它们可能会有所不同。

评论

2赞 Frank Krueger 9/18/2008
你有权威的参考资料或演示吗?
1赞 Dark Shikari 9/18/2008 #2

为了回应弗兰克·克鲁格(Frank Krueger)在上面(在评论中)提出的差异演示请求:

在没有优化和 -mfpmath=387 的情况下在 gcc 中编译此代码(我没有理由认为它不适用于其他编译器,但我没有尝试过。 然后在不进行优化的情况下编译它,并使用 -msse -mfpmath=sse。

输出会有所不同。

#include <stdio.h>

int main()
{
    float e = 0.000000001;
    float f[3] = {33810340466158.90625,276553805316035.1875,10413022032824338432.0};
    f[0] = pow(f[0],2-e); f[1] = pow(f[1],2+e); f[2] = pow(f[2],-2-e);
    printf("%s\n",f);
    return 0;
}

评论

1赞 James Curran 9/18/2008
问题是关于 C#/.Net;您的示例适用于 C++/本机代码。
1赞 Dark Shikari 9/18/2008
不知何故,我怀疑 SSE 与 x87 FPU 的精度是否因您调用它的语言而异!
0赞 Asik 2/13/2014
直接转换为 C# 还会显示 Visual Studio 2013 中 x86 和 x64 的不同结果。请注意,x86 CLR 使用 x87 FPU,而 x64 CLR 使用 SSE。
22赞 stusmith 9/18/2008 #3

它们确实可以不同。根据 CLR ECMA 规范:

浮点的存储位置 数字(statics、数组元素和 类的字段)的大小是固定的。 支持的存储大小为 float32 和 float64。其他任何地方 (在评估堆栈上,如 参数,作为返回类型,以及 局部变量) 浮点数 数字用 内部浮点类型。在每个 这样的实例,标称类型的 变量或表达式是 R4 或 R8,但其值可以表示 内部具有额外的范围 和/或精度。的大小 内部浮点表示 取决于实现,可能会有所不同, 并且精度至少应为 与变量或 表示的表达式。一 隐式扩展转换为 float32 的内部表示 或 float64 在执行这些 类型是从存储中加载的。这 内部表示通常为 硬件的本机大小,或 根据效率要求 操作的实现。

这基本上意味着以下比较可能相等,也可能不相等:

class Foo
{
  double _v = ...;

  void Bar()
  {
    double v = _v;

    if( v == _v )
    {
      // Code may or may not execute here.
      // _v is 64-bit.
      // v could be either 64-bit (debug) or 80-bit (release) or something else (future?).
    }
  }
}

带回家的信息:永远不要检查浮动值是否相等。

评论

0赞 Greg Beech 9/18/2008
不过,这与 DEBUG 与 RELEASE 构建配置没有任何关系......
2赞 stusmith 9/22/2008
生成的 IL 将是相同的......但是 JITter 在处理标记为调试的程序集时不那么激进。发布版本倾向于将更多浮点值移动到 80 位寄存器中;调试版本倾向于直接从 64 位内存存储中读取。
0赞 ShuggyCoUk 2/24/2009
生成的 IL 可能不同。调试模式在某个位置插入 NOP 以确保断点是可能的,它也可能故意维护释放模式认为不必要的临时变量。
11赞 Skizz 9/18/2008 #4

这是一个有趣的问题,所以我做了一些实验。我使用了以下代码:

static void Main (string [] args)
{
  float
    a = float.MaxValue / 3.0f,
    b = a * a;

  if (a * a < b)
  {
    Console.WriteLine ("Less");
  }
  else
  {
    Console.WriteLine ("GreaterEqual");
  }
}

使用 DevStudio 2005 和 .Net 2。我编译为调试和发布,并检查了编译器的输出:

Release                                                    Debug

    static void Main (string [] args)                        static void Main (string [] args)
    {                                                        {
                                                        00000000  push        ebp  
                                                        00000001  mov         ebp,esp 
                                                        00000003  push        edi  
                                                        00000004  push        esi  
                                                        00000005  push        ebx  
                                                        00000006  sub         esp,3Ch 
                                                        00000009  xor         eax,eax 
                                                        0000000b  mov         dword ptr [ebp-10h],eax 
                                                        0000000e  xor         eax,eax 
                                                        00000010  mov         dword ptr [ebp-1Ch],eax 
                                                        00000013  mov         dword ptr [ebp-3Ch],ecx 
                                                        00000016  cmp         dword ptr ds:[00A2853Ch],0 
                                                        0000001d  je          00000024 
                                                        0000001f  call        793B716F 
                                                        00000024  fldz             
                                                        00000026  fstp        dword ptr [ebp-40h] 
                                                        00000029  fldz             
                                                        0000002b  fstp        dword ptr [ebp-44h] 
                                                        0000002e  xor         esi,esi 
                                                        00000030  nop              
      float                                                      float
        a = float.MaxValue / 3.0f,                                a = float.MaxValue / 3.0f,
00000000  sub         esp,0Ch                            00000031  mov         dword ptr [ebp-40h],7EAAAAAAh
00000003  mov         dword ptr [esp],ecx                
00000006  cmp         dword ptr ds:[00A2853Ch],0        
0000000d  je          00000014                            
0000000f  call        793B716F                            
00000014  fldz                                            
00000016  fstp        dword ptr [esp+4]                    
0000001a  fldz                                            
0000001c  fstp        dword ptr [esp+8]                    
00000020  mov         dword ptr [esp+4],7EAAAAAAh        
        b = a * a;                                                b = a * a;
00000028  fld         dword ptr [esp+4]                    00000038  fld         dword ptr [ebp-40h] 
0000002c  fmul        st,st(0)                            0000003b  fmul        st,st(0) 
0000002e  fstp        dword ptr [esp+8]                    0000003d  fstp        dword ptr [ebp-44h] 

      if (a * a < b)                                          if (a * a < b)
00000032  fld         dword ptr [esp+4]                    00000040  fld         dword ptr [ebp-40h] 
00000036  fmul        st,st(0)                            00000043  fmul        st,st(0) 
00000038  fld         dword ptr [esp+8]                    00000045  fld         dword ptr [ebp-44h] 
0000003c  fcomip      st,st(1)                            00000048  fcomip      st,st(1) 
0000003e  fstp        st(0)                                0000004a  fstp        st(0) 
00000040  jp          00000054                            0000004c  jp          00000052 
00000042  jbe         00000054                            0000004e  ja          00000056 
                                                        00000050  jmp         00000052 
                                                        00000052  xor         eax,eax 
                                                        00000054  jmp         0000005B 
                                                        00000056  mov         eax,1 
                                                        0000005b  test        eax,eax 
                                                        0000005d  sete        al   
                                                        00000060  movzx       eax,al 
                                                        00000063  mov         esi,eax 
                                                        00000065  test        esi,esi 
                                                        00000067  jne         0000007A 
      {                                                          {
        Console.WriteLine ("Less");                        00000069  nop              
00000044  mov         ecx,dword ptr ds:[0239307Ch]                Console.WriteLine ("Less");
0000004a  call        78678B7C                            0000006a  mov         ecx,dword ptr ds:[0239307Ch] 
0000004f  nop                                            00000070  call        78678B7C 
00000050  add         esp,0Ch                            00000075  nop              
00000053  ret                                                  }
      }                                                    00000076  nop              
      else                                                00000077  nop              
      {                                                    00000078  jmp         00000088 
        Console.WriteLine ("GreaterEqual");                      else
00000054  mov         ecx,dword ptr ds:[02393080h]              {
0000005a  call        78678B7C                            0000007a  nop              
      }                                                            Console.WriteLine ("GreaterEqual");
    }                                                    0000007b  mov         ecx,dword ptr ds:[02393080h] 
                                                        00000081  call        78678B7C 
                                                        00000086  nop              
                                                              }

上面显示的是,调试和发布的浮点代码是相同的,编译器选择一致性而不是优化。尽管程序产生错误的结果(a * a 不小于 b),但无论调试/发布模式如何,结果都是一样的。

现在,英特尔 IA32 FPU 有 8 个浮点寄存器,您可能会认为编译器在优化时会使用这些寄存器来存储值,而不是写入内存,从而提高性能,大致如下:

fld         dword ptr [a] ; precomputed value stored in ram == float.MaxValue / 3.0f
fmul        st,st(0) ; b = a * a
; no store to ram, keep b in FPU
fld         dword ptr [a]
fmul        st,st(0)
fcomi       st,st(0) ; a*a compared to b

但这与调试版本的执行方式不同(在本例中,显示正确的结果)。但是,根据构建选项更改程序的行为是一件非常糟糕的事情。

FPU 代码是手工制作代码可以明显优于编译器的一个领域,但您确实需要了解 FPU 的工作方式。

2赞 fuglede 3/28/2019 #5

下面是一个简单的示例,其中结果不仅在调试模式和发布模式之间有所不同,而且它们这样做的方式取决于使用 x86 还是 x84 作为平台:

Single f1 = 0.00000000002f;
Single f2 = 1 / f1;
Double d = f2;
Console.WriteLine(d);

这将写入以下结果:

            Debug       Release
x86   49999998976   50000000199,7901
x64   49999998976   49999998976

快速浏览一下反汇编(Visual Studio 中的“调试”->“Windows”->“反汇编”)提供了一些有关此处发生的情况的提示。对于 x86 情况:

Debug                                       Release
mov         dword ptr [ebp-40h],2DAFEBFFh | mov         dword ptr [ebp-4],2DAFEBFFh  
fld         dword ptr [ebp-40h]           | fld         dword ptr [ebp-4]   
fld1                                      | fld1
fdivrp      st(1),st                      | fdivrp      st(1),st
fstp        dword ptr [ebp-44h]           |
fld         dword ptr [ebp-44h]           |
fstp        qword ptr [ebp-4Ch]           |
fld         qword ptr [ebp-4Ch]           |
sub         esp,8                         | sub         esp,8 
fstp        qword ptr [esp]               | fstp        qword ptr [esp]
call        6B9783BC                      | call        6B9783BC

特别是,我们看到一堆看似多余的“将浮点寄存器中的值存储在内存中,然后立即将其从内存加载回浮点寄存器”在释放模式下进行了优化。但是,这两个说明

fstp        dword ptr [ebp-44h]  
fld         dword ptr [ebp-44h]

足以将 x87 寄存器中的值从 +5.0000000199790138e+0010 更改为 +4.9999998976000000e+0010,因为可以通过单步拆卸和调查相关寄存器的值来验证(调试 -> Windows ->寄存器,然后右键单击并选中“浮点”)。

x64 的情况截然不同。我们仍然看到相同的优化删除了一些指令,但这一次,一切都依赖于 SSE 及其 128 位寄存器和专用指令集:

Debug                                        Release
vmovss      xmm0,dword ptr [7FF7D0E104F8h] | vmovss      xmm0,dword ptr [7FF7D0E304C8h]  
vmovss      dword ptr [rbp+34h],xmm0       | vmovss      dword ptr [rbp-4],xmm0 
vmovss      xmm0,dword ptr [7FF7D0E104FCh] | vmovss      xmm0,dword ptr [7FF7D0E304CCh]
vdivss      xmm0,xmm0,dword ptr [rbp+34h]  | vdivss      xmm0,xmm0,dword ptr [rbp-4]
vmovss      dword ptr [rbp+30h],xmm0       |
vcvtss2sd   xmm0,xmm0,dword ptr [rbp+30h]  | vcvtss2sd   xmm0,xmm0,xmm0 
vmovsd      qword ptr [rbp+28h],xmm0       |
vmovsd      xmm0,qword ptr [rbp+28h]       |
call        00007FF81C9343F0               | call        00007FF81C9343F0 

在这里,由于 SSE 单元避免在内部使用比单精度更高的精度(而 x87 单元这样做),因此无论优化如何,我们最终都会得到 x86 情况的“单精度”结果。事实上,人们发现(在 Visual Studio 寄存器概述中启用 SSE 寄存器后)之后,XMM0 包含 000000000000000000-000000000513A43B7,这与之前的49999998976完全相同。vdivss

在实践中,这两种差异都让我感到困扰。除了说明永远不应该比较浮点的相等性之外,该示例还表明,在浮点出现的那一刻,在高级语言(如 C#)中仍有程序集调试的空间。