为什么代码行为在发布和调试模式下不同?
Why is code behavior different in release & debug mode?
考虑以下代码:
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 进行测试。
这似乎是一个 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.3
和 call 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
确实是一个汇编错误。 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,jg 00007FF942690AE7。如果 ebx(包含 4,循环结束值)大于 (jg) eax,即值 i,它会跳回。当它是 4 时,这当然会失败,因此它不会写入数组中的最后一个元素。
它失败了,因为它包含了 i 的寄存器值(eax,在 0x00007FF942690AF9),然后用 4 检查它,但它仍然必须写入那个值。很难确定问题的确切位置,因为它看起来可能是 (N-Old.Length) 优化的结果,因为调试版本包含该代码,但发布版本预先计算了.这就是 jit 人员需要解决的问题 ;)
考虑以下代码:
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 进行测试。
这似乎是一个 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.3
和 call 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
确实是一个汇编错误。 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,jg 00007FF942690AE7。如果 ebx(包含 4,循环结束值)大于 (jg) eax,即值 i,它会跳回。当它是 4 时,这当然会失败,因此它不会写入数组中的最后一个元素。
它失败了,因为它包含了 i 的寄存器值(eax,在 0x00007FF942690AF9),然后用 4 检查它,但它仍然必须写入那个值。很难确定问题的确切位置,因为它看起来可能是 (N-Old.Length) 优化的结果,因为调试版本包含该代码,但发布版本预先计算了.这就是 jit 人员需要解决的问题 ;)