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

考虑以下代码:

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,100,0。

发生什么事了?

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

5090 次浏览

This appears to be a JIT bug; I've tested with:

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

and adding the Console.WriteLine(i) fixes it. The only IL change is:

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

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

which looks exactly right (the only difference is the extra ldloc.3 and call void [System.Console]System.Console::WriteLine(int32), and a different but equivalent target for br.s).

It'll need a JIT fix, I suspect.

Environment:

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

It's an assembly error indeed. x64, .net 4.7.1, release build.

disassembly:

            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

The issue is at address 00007FF942690AFD, the jg 00007FF942690AE7. It jumps back if ebx (which contains 4, the loop end value) is bigger (jg) than eax, the value i. This fails when it's 4 of course, so it doesn't write the last element in the array.

It fails, because it inc's i's register value (eax, at 0x00007FF942690AF9), and then checks it with 4, but it still has to write that value. It's a bit hard to pinpoint where exactly the issue is located, as it looks like it might be the result of the optimization of (N-Old.Length), as the debug build contains that code, but the release build precalculates that. So that's for the jit people to fix ;)