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


84

考虑以下代码:

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;
}

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

怎么了?

已使用.NET Framework 4.7.1和.NET Core 2.0.0进行了测试。


您使用哪个版本的Visual Studio(或编译器)?
Styxxy

9
复制 添加Console.WriteLine(i);到最终的环(dd[i] = d;)“修复”它,这表明编译器故障或错误JIT; 望着IL ...
Marc Gravell

@Styxxy,在vs2015、2017上进行了测试,并针对所有大于
等于

绝对是一个错误。如果将if (dd.Length >= N) return;其删除,它也会消失,这可能是更简单的复制。
Jeroen Mostert

1
毫不奇怪,一旦比较成功,.Net Framework和.Net Core的x64代码生成器将具有类似的性能,因为(默认情况下)它实际上是相同的jit生成代码。将.Net Framework x86代码生成器的性能与.Net Core的x86代码生成器(从2.0开始使用RyuJit)进行比较,将是很有趣的。在某些情况下,较旧的jit(又名Jit32)知道一些RyuJit所不知道的技巧。并且,如果您发现任何此类情况,请确保在CoreCLR存储库上为他们解决问题。
安迪·艾尔斯

Answers:


70

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

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

并添加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 

// ...
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 

看起来完全正确(唯一的区别是额外的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

然后在哪里报告错误?
Ashkan Nourzadeh '17

1
我也在.NET full 4.7.1上看到了它,因此,如果这不是RyuJIT的错误,我会吃掉我的帽子。
Jeroen Mostert

2
我无法复制,安装了.NET 4.7.1,现在可以复制。
user3057557'1

3
@MarcGravell .Net框架4.7.1和.net Core 2.0.0
Ashkan Nourzadeh

4
@AshkanNourzadeh ,老实说,我可能会将其记录在这里,强调人们认为这是RyuJIT错误
Marc Gravell

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  

问题位于地址00007FF942690AFD,编号为00007FF942690AE7。如果ebx(包含4,循环结束值)比eax(值i)大(jg),则会跳回。当然,这在4时会失败,因此它不会写入数组中的最后一个元素。

它失败了,因为它是我的寄存器值(eax,位于0x00007FF942690AF9),然后用4对其进行检查,但是它仍然必须写入该值。查明问题的确切位置有点困难,因为它看起来可能是(N-Old.Length)优化的结果,因为调试版本包含该代码,但发行版本对此进行了预先计算。因此,这是供jit人士解决的;)


2
这些天之一,我需要花一些时间来学习汇编/ CPU操作码。也许天真地我一直在思考“嗯,我可以读写IL-我应该可以理解它”-但是我从来没有绕过它:)
Marc Gravell

x64 / x86并不是以tho开头的最出色的汇编语言;)它有太多的操作码,我曾经读过,没有人知道所有这些。不知道这是真的,但是一开始阅读起来并不容易。尽管它确实使用了一些简单的约定,例如[],源部分之前的目的地以及这些寄存器的含义(al是rax的8位部分,eax是rax的32位部分等)。您可以在tho中逐步了解它,它应该教会您基本知识。我敢肯定,因为您已经知道IL操作码,所以您会很快注意到它;)
Frans Bouma
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.