CLR JIT优化是否违反了因果关系?

时间:2010-02-08 22:52:10

标签: .net floating-point clr equality ieee-754

我正在为一位同事写一个有启发性的例子,告诉他为什么测试花车的平等往往是一个坏主意。我参加的例子是增加.1十次,并与1.0(我在介绍性数字课中显示的那个)进行比较。我惊讶地发现两个结果相等code + output)。

float @float = 0.0f;
for(int @int = 0; @int < 10; @int += 1)
{
    @float += 0.1f;
}
Console.WriteLine(@float == 1.0f);

一些调查表明,这个结果无法依赖(很像浮动平等)。我发现最令人惊讶的是在之后添加代码可能会改变计算结果(code + output)。请注意,此示例具有完全相同的代码和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"));

我知道我不应该在花车上使用平等,因此不应该太在意这一点,但我发现它非常令人惊讶,就像我所展示的每个人一样。在执行计算后执行之后的事情会改变前面计算的值吗?我不认为这是人们通常在脑海中计算的模型。

我并不完全难过,似乎可以肯定地假设在“相等”情况下发生某种优化会改变计算结果(在调试模式下构建会阻止“相等”情况)。显然,当CLR发现以后需要将浮动框打包时,优化​​将被放弃。

我搜索了一下但找不到这种行为的原因。任何人都可以提醒我吗?

4 个答案:

答案 0 :(得分:18)

这是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 

当您添加额外的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 

注意地址15与地址17 + 1a的差异,第一个循环将中间结果保留在FPU中。第二个循环将其存储回@float局部变量。当它停留在FPU内部时,结果将以完全精度计算。然而,将它存储回来会将中间结果截断回浮点数,从而在过程中丢失大量精度。

虽然不愉快,但我不相信这是一个错误。 x64 JIT编译器的行为方式不同。您可以在connect.microsoft.com

中创建案例

答案 1 :(得分:6)

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

答案 2 :(得分:4)

您是否在英特尔处理器上运行此操作?

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

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

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

修改

唔... 我在本地测试了你的代码(英特尔酷睿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.

x64和x86 JIT编译器之间存在 差异,但我无法访问32位机器。

答案 3 :(得分:2)

我的理论是,如果没有ToString行,编译器能够将函数静态优化为单个值,并以某种方式补偿浮点错误。但是当添加ToString行时,优化器必须以不同方式处理浮点数,因为方法调用需要它。这只是猜测。