[c#] 릴리스 및 디버그 모드에서 코드 동작이 다른 이유는 무엇입니까?

다음 코드를 고려하십시오.

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

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

정확히 옳게 보입니다 (유일한 차이점은 여분의 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


답변

실제로 어셈블리 오류입니다. 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 포함)가 값 i 인 eax보다 크면 (jg) 뒤로 이동합니다. 물론 4이면 실패하므로 배열의 마지막 요소를 쓰지 않습니다.

inc가 i의 레지스터 값 (eax, 0x00007FF942690AF9)이므로 실패하고 4로 확인하지만 여전히 해당 값을 써야합니다. 디버그 빌드에 해당 코드가 포함되어 있기 때문에 (N-Old.Length) 최적화의 결과 인 것처럼 보이기 때문에 문제가 정확히 어디에 있는지 정확히 찾아내는 것은 약간 어렵지만 릴리스 빌드는이를 미리 계산합니다. 그래서 그것은 jit 사람들이 고칠 것입니다;)


답변