Float/double precision in debug/release modes
在调试模式和发布模式之间,C#/。NET浮点运算的精度是否有所不同?
它们确实可以不同。根据CLR ECMA规范:
Storage locations for floating-point
numbers (statics, array elements, and
fields of classes) are of fixed size.
The supported storage sizes are
float32 and float64. Everywhere else
(on the evaluation stack, as
arguments, as return types, and as
local variables) floating-point
numbers are represented using an
internal floating-point type. In each
such instance, the nominal type of the
variable or expression is either R4 or
R8, but its value can be represented
internally with additional range
and/or precision. The size of the
internal floating-point representation
is implementation-dependent, can vary,
and shall have precision at least as
great as that of the variable or
expression being represented. An
implicit widening conversion to the
internal representation from float32
or float64 is performed when those
types are loaded from storage. The
internal representation is typically
the native size for the hardware, or
as required for efficient
implementation of an operation.
这基本上意味着以下比较可能相等或不相等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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?).
}
}
} |
提示:永远不要检查浮点值是否相等。
这是一个有趣的问题,所以我做了一些实验。我使用以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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和.Net2。我将其编译为调试和发布版本,并检查了编译器的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| 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),但无论调试/释放模式如何,该结果都是相同的。
现在,Intel IA32 FPU具有八个浮点寄存器,您会认为编译器在优化时将使用寄存器来存储值,而不是写入内存,从而提高了性能,大致如下:
1 2 3 4 5 6
| 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的工作方式。
实际上,如果调试模式使用x87 FPU,而释放模式使用SSE进行浮点运算,则它们可能会有所不同。
这是一个简单的示例,其中结果不仅在调试和发布模式之间有所不同,而且结果的方式取决于是否使用x86或x84作为平台:
1 2 3 4
| Single f1 = 0.00000000002f;
Single f2 = 1 / f1;
Double d = f2;
Console.WriteLine(d); |
这将写入以下结果:
1 2 3
| Debug Release
x86 49999998976 50000000199,7901
x64 49999998976 49999998976 |
快速查看反汇编(在Visual Studio中为"调试"->" Windows"->"反汇编"),可以了解此处的情况。对于x86情况:
1 2 3 4 5 6 7 8 9 10 11 12
| 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 |
特别是,我们看到一堆看似多余的"将浮点寄存器中的值存储在内存中,然后立即将其从内存中加载回浮点寄存器中"已在释放模式下进行了优化。但是,这两个指令
1 2
| fstp dword ptr [ebp-44h]
fld dword ptr [ebp-44h] |
足以将x87寄存器中的值从+ 5.0000000199790138e + 0010更改为+ 4.9999998976000000e + 0010,因为可以通过逐步反汇编并研究相关寄存器的值来进行验证(调试-> Windows->寄存器,然后右击点击并选中"浮点数")。
x64的故事截然不同。我们仍然看到相同的优化删除了一些指令,但是这次,一切都依赖于SSE及其128位寄存器和专用指令集:
1 2 3 4 5 6 7 8 9 10
| 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情况的"单精度-ish"结果。确实,发现(在Visual Studio寄存器概述中启用了SSE寄存器之后),在vdivss之后,XMM0包含0000000000000000-00000000513A43B7,与之前的49999998976完全相同。
这两种差异在实践中都使我难过。除了说明不应该比较浮点数相等之外,该示例还显示,在浮点数出现的那一刻,仍然可以使用C#等高级语言进行汇编调试。
为了回应弗兰克·克鲁格(Frank Krueger)的上述要求(在评论中),以证明有区别:
在没有优化和-mfpmath = 387的情况下在gcc中编译此代码(我没有理由认为它不能在其他编译器上运行,但是我没有尝试过。)
然后,不进行任何优化和-msse -mfpmath = sse进行编译。
输出将有所不同。
1 2 3 4 5 6 7 8 9 10 11
| #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\
",f);
return 0;
} |
|