【问题标题】:CLR JIT optimizations violates causality?CLR JIT 优化违反因果关系?
【发布时间】:2010-02-08 22:52:10
【问题描述】:

我正在为一位同事写一个有启发性的示例,向他展示为什么测试浮点数的相等性通常是一个坏主意。我使用的示例是添加 0.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 发现稍后需要对浮点数进行装箱时,优化就被放弃了。

我进行了一些搜索,但找不到这种行为的原因。谁能帮我解答一下?

【问题讨论】:

  • 也很有趣:如果我 fork your first example 并在没有任何更改的情况下运行它,我将不再得到您最初看到的结果。

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


【解决方案1】:

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

                @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 上提出您的意见

【讨论】:

  • 好答案。我也不会称其为错误。更多的是不寻常的(而且绝对是出乎意料的)副作用。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    您是否在 Intel 处理器上运行此程序?

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

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

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

    编辑:

    嗯... 我在本地测试了您的代码(英特尔酷睿 2、Windows 7 x64、64 位 CLR),但总是出现“预期的”舍入错误。在发布和调试配置中。

    以下是我机器上第一个代码sn-p的反汇编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 位机器。

    【讨论】:

    • 我确实在英特尔处理器上运行过它。不过,我不知道 dotnetpad 是否在 Intel 处理器上运行。
    【解决方案4】:

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

    【讨论】:

      猜你喜欢
      • 2013-10-22
      • 2016-07-30
      • 1970-01-01
      • 1970-01-01
      • 2010-09-21
      • 2011-12-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多