为什么在发布和调试模式下代码行为有所不同?

Ash*_*deh 82 .net c# .net-core coreclr

请考虑以下代码:

private static void Main(string[] args)
{
    var ar = new double[]
    {
        100
    };

    FillTo(ref ar, 5);
    Console.WriteLine(string.Join(",", ar.Select(a => a.ToString()).ToArray()));
}

public static void FillTo(ref double[] dd, int N)
{
    if (dd.Length >= N)
        return;

    double[] Old = dd;
    double d = double.NaN;
    if (Old.Length > 0)
        d = Old[0];

    dd = new double[N];

    for (int i = 0; i < Old.Length; i++)
    {
        dd[N - Old.Length + i] = Old[i];
    }
    for (int i = 0; i < N - Old.Length; i++)
        dd[i] = d;
}
Run Code Online (Sandbox Code Playgroud)

调试模式的结果是:100,100,100,100,100.但在发布模式下,它是:100,100,100,100,0.

怎么了?

它使用.NET framework 4.7.1和.NET Core 2.0.0进行了测试.

Mar*_*ell 69

这似乎是一个JIT错误; 我测试过:

// ... existing code unchanged
for (int i = 0; i < N - Old.Length; i++)
{
    // Console.WriteLine(i); // <== comment/uncomment this line
    dd[i] = d;
}
Run Code Online (Sandbox Code Playgroud)

并添加Console.WriteLine(i)修复它.唯一的IL变化是:

// ...
L_0040: ldc.i4.0 
L_0041: stloc.3 
L_0042: br.s L_004d
L_0044: ldarg.0 
L_0045: ldind.ref 
L_0046: ldloc.3 
L_0047: ldloc.1 
L_0048: stelem.r8 
L_0049: ldloc.3 
L_004a: ldc.i4.1 
L_004b: add 
L_004c: stloc.3 
L_004d: ldloc.3 
L_004e: ldarg.1 
L_004f: ldloc.0 
L_0050: ldlen 
L_0051: conv.i4 
L_0052: sub 
L_0053: blt.s L_0044
L_0055: ret 
Run Code Online (Sandbox Code Playgroud)

VS

// ...
L_0040: ldc.i4.0 
L_0041: stloc.3 
L_0042: br.s L_0053
L_0044: ldloc.3 
L_0045: call void [System.Console]System.Console::WriteLine(int32)
L_004a: ldarg.0 
L_004b: ldind.ref 
L_004c: ldloc.3 
L_004d: ldloc.1 
L_004e: stelem.r8 
L_004f: ldloc.3 
L_0050: ldc.i4.1 
L_0051: add 
L_0052: stloc.3 
L_0053: ldloc.3 
L_0054: ldarg.1 
L_0055: ldloc.0 
L_0056: ldlen 
L_0057: conv.i4 
L_0058: sub 
L_0059: blt.s L_0044
L_005b: ret 
Run Code Online (Sandbox Code Playgroud)

这看起来完全正确的(唯一的区别是多余的ldloc.3call void [System.Console]System.Console::WriteLine(int32),以及不同但等效的目标br.s).

我怀疑它需要一个JIT修复.

环境:

  • Environment.Version:4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS:15.5.0预览5.0
  • dotnet --version:2.1.1

  • @AshkanNourzadeh我可能会记录它[这里](https://github.com/dotnet/coreclr)说实话,强调人们认为这是一个RyuJIT错误 (4认同)
  • @MarcGravell .Net framework 4.7.1和.net Core 2.0.0 (3认同)
  • 我无法重现,安装了.NET 4.7.1并且现在可以重现. (2认同)

Fra*_*uma 6

这确实是一个装配错误.x64,.net 4.7.1,发布版本.

拆卸:

            for(int i = 0; i < N - Old.Length; i++)
00007FF942690ADD  xor         eax,eax  
            for(int i = 0; i < N - Old.Length; i++)
00007FF942690ADF  mov         ebx,esi  
00007FF942690AE1  sub         ebx,ebp  
00007FF942690AE3  test        ebx,ebx  
00007FF942690AE5  jle         00007FF942690AFF  
                dd[i] = d;
00007FF942690AE7  mov         rdx,qword ptr [rdi]  
00007FF942690AEA  cmp         eax,dword ptr [rdx+8]  
00007FF942690AED  jae         00007FF942690B11  
00007FF942690AEF  movsxd      rcx,eax  
00007FF942690AF2  vmovsd      qword ptr [rdx+rcx*8+10h],xmm6  
            for(int i = 0; i < N - Old.Length; i++)
00007FF942690AF9  inc         eax  
00007FF942690AFB  cmp         ebx,eax  
00007FF942690AFD  jg          00007FF942690AE7  
00007FF942690AFF  vmovaps     xmm6,xmmword ptr [rsp+20h]  
00007FF942690B06  add         rsp,30h  
00007FF942690B0A  pop         rbx  
00007FF942690B0B  pop         rbp  
00007FF942690B0C  pop         rsi  
00007FF942690B0D  pop         rdi  
00007FF942690B0E  pop         r14  
00007FF942690B10  ret  
Run Code Online (Sandbox Code Playgroud)

问题发生在地址00007FF942690AFD,即00007FF942690AE7.如果ebx(包含4,循环结束值)比eax(jg)更大(jg),则值会跳回.当它为4时失败,因此它不会写入数组中的最后一个元素.

它失败了,因为它是我的寄存器值(eax,在0x00007FF942690AF9),然后用4检查它,但它仍然必须写入该值.有点难以确定问题的确切位置,因为它看起来可能是(N-Old.Length)优化的结果,因为调试版本包含该代码,但是版本构建预先计算了.那就是jit人要解决的问题;)

  • 有一天我需要花些时间来学习汇编/ CPU操作码.也许是天真的我一直在想"嗯,我可以读写IL - 我应该能够理解它" - 但我从来没有绕过它:) (2认同)