.net CLR JIT优化违反因果关系?

.net CLR JIT优化违反因果关系?,.net,floating-point,clr,equality,ieee-754,.net,Floating Point,Clr,Equality,Ieee 754,我正在给一位同事写一个很有启发性的例子,向他说明为什么测试浮动是否相等通常是个坏主意。我使用的例子是添加.1十次,并与1.0(我在介绍性数字课程中展示的那一个)进行比较。我惊讶地发现这两个结果是相等的() float@float=0.0f; 对于(int@int=0;@int

我正在给一位同事写一个很有启发性的例子,向他说明为什么测试浮动是否相等通常是个坏主意。我使用的例子是添加.1十次,并与1.0(我在介绍性数字课程中展示的那一个)进行比较。我惊讶地发现这两个结果是相等的()

float@float=0.0f;
对于(int@int=0;@int<10;@int+=1)
{
@浮点数+=0.1f;
}
Console.WriteLine(@float==1.0f);
一些调查表明,这一结果不可靠(很像浮动相等)。我发现最令人惊讶的是,在另一个代码之后添加代码可能会改变计算结果()。注意,这个例子有完全相同的代码和IL,附加了一行C

float @float = 0.0f;
for(int @int = 0; @int < 10; @int += 1)
{
    @float += 0.1f;
}
Console.WriteLine(@float == 1.0f);
Console.WriteLine(@float.ToString("G9"));
float@float=0.0f;
对于(int@int=0;@int<10;@int+=1)
{
@浮点数+=0.1f;
}
Console.WriteLine(@float==1.0f);
Console.WriteLine(@float.ToString(“G9”));
我知道我不应该在浮点数上使用相等,因此不应该太在意这个问题,但我发现这非常令人惊讶,就像我向所有人展示这一点一样。执行计算后执行操作会更改前面计算的值吗?我不认为这是人们通常想到的计算模型

我并没有完全被难倒,似乎可以安全地假设在“相等”的情况下会发生某种优化,从而改变计算结果(在调试模式下构建可以防止“相等”的情况)。显然,当CLR发现以后需要装箱浮点时,就放弃了优化


我搜索了一下,但找不到这种行为的原因。有人能给我提供线索吗?

你是在英特尔处理器上运行的吗

一种理论是,JIT允许将
@float
完全累加到浮点寄存器中,这将是80位的完整精度。这样计算就足够精确了

第二个版本的代码不能完全放入寄存器,因此
@float
必须“溢出”到内存,这会导致80位值向下舍入到单精度,从而给出单精度算术的预期结果

但这只是一个非常随机的猜测。必须检查JIT编译器生成的实际机器代码(打开反汇编视图进行调试)

编辑:

嗯。。。 我在本地测试了您的代码(Intel Core 2、Windows 7 x64、64位CLR),始终得到“预期”舍入错误。在发行版和调试配置中

以下是Visual Studio为我的计算机上的第一个代码段显示的反汇编:

xorps       xmm0,xmm0 
movss       dword ptr [rsp+20h],xmm0 
        for (int @int = 0; @int < 10; @int += 1)
mov         dword ptr [rsp+24h],0 
jmp         0000000000000061 
        {
            @float += 0.1f;
movss       xmm0,dword ptr [000000A0h] 
addss       xmm0,dword ptr [rsp+20h] 
movss       dword ptr [rsp+20h],xmm0 // <-- @float gets stored in memory
        for (int @int = 0; @int < 10; @int += 1)
mov         eax,dword ptr [rsp+24h] 
add         eax,1 
mov         dword ptr [rsp+24h],eax 
cmp         dword ptr [rsp+24h],0Ah 
jl          0000000000000042 
        }
        Console.WriteLine(@float == 1.0f);
etc.
xorps xmm0,xmm0 movss dword ptr[rsp+20h],xmm0 对于(int@int=0;@int<10;@int+=1) 移动dword ptr[rsp+24小时],0 jmp 00000000000000 61 { @浮点数+=0.1f; movss xmm0,dword ptr[000000 a0h] 地址xmm0,dword ptr[rsp+20h]
movss dword ptr[rsp+20小时],xmm0/我的理论是,如果没有ToString行,编译器可以静态优化函数,使其降为单个值,并以某种方式补偿浮点错误。但是当添加ToString行时,优化器必须以不同的方式处理浮点,因为方法调用需要它。这只是猜测

这是JIT优化器工作方式的一个副作用。如果要生成的代码较少,它会做更多的工作。原始代码段中的循环将编译为:

                @float += 0.1f;
0000000f  fld         dword ptr ds:[0025156Ch]          ; push(intermediate), st0 = 0.1
00000015  faddp       st(1),st                          ; st0 = st0 + st1
            for (int @int = 0; @int < 10; @int += 1) {
00000017  inc         eax  
00000018  cmp         eax,0Ah 
0000001b  jl          0000000F 
@float+=0.1f;
0000000 F fld dword ptr ds:[0025156 CH];推送(中间),st0=0.1
000000 15 faddp st(1),st;st0=st0+st1
对于(int@int=0;@int<10;@int+=1){
00000017公司
000000 18 cmp eax,0Ah
0000001b jl 0000000 f
添加额外的Console.WriteLine()语句时,它会将其编译为:

                @float += 0.1f;
00000011  fld         dword ptr ds:[00961594h]          ; st0 = 0.1
00000017  fadd        dword ptr [ebp-8]                 ; st0 = st0 + @float
0000001a  fstp        dword ptr [ebp-8]                 ; @float = st0
            for (int @int = 0; @int < 10; @int += 1) {
0000001d  inc         eax  
0000001e  cmp         eax,0Ah 
00000021  jl          00000011 
@float+=0.1f;
000000 11 fld dword ptr ds:[00961594h];st0=0.1
00000017 fadd dword ptr[ebp-8];st0=st0+@float
0000001a fstp dword ptr[ebp-8];@float=st0
对于(int@int=0;@int<10;@int+=1){
0000001d公司eax
0000001e cmp eax,0Ah
00000021 JL00000011
请注意地址15与地址17+1a之间的差异,第一个循环将中间结果保留在FPU中。第二个循环将其存储回@float局部变量。当它保留在FPU中时,结果以全精度计算。但是,将其存储回FPU会将中间结果截断回浮点,从而丢失大量位of过程中的精度

虽然令人不快,但我不认为这是一个bug。x64 JIT编译器的行为仍然不同。您可以在connect.microsoft.com

供参考,C#规范指出这种行为是合法和常见的。有关更多详细信息和类似场景,请参阅这些问题:


我肯定在英特尔处理器上运行过它。不过,我不知道dotnetpad是否在英特尔处理器上运行。回答得好。我也不会说它是一个bug。更多的是一个不寻常的(绝对出乎意料的)副作用。还有一点很有趣:如果我不做任何更改就运行它,我将不再得到您最初看到的结果。
                @float += 0.1f;
00000011  fld         dword ptr ds:[00961594h]          ; st0 = 0.1
00000017  fadd        dword ptr [ebp-8]                 ; st0 = st0 + @float
0000001a  fstp        dword ptr [ebp-8]                 ; @float = st0
            for (int @int = 0; @int < 10; @int += 1) {
0000001d  inc         eax  
0000001e  cmp         eax,0Ah 
00000021  jl          00000011