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


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
Repro; 를 첨가 Console.WriteLine(i);(최종 루프 dd[i] = d;) 컴파일러 버그 JIT 버그 제안 "수정"을; IL 조사 ...
Marc Gravell

@Styxxy, vs2015, 2017에서 테스트되었으며 모든 .net 프레임 워크를 대상으로> = 4.5
Ashkan Nourzadeh

확실히 버그입니다. 제거 if (dd.Length >= N) return;하는 경우에도 사라집니다 . 이는 더 간단한 재현 일 수 있습니다.
Jeroen Mostert

1
닷넷 프레임 워크와 .Net Core 용 x64 코드 생성이 비슷한 성능을 가지게되는데, 이는 기본적으로 기본적으로 동일한 jit 생성 코드이기 때문입니다. .Net Framework x86 코드 젠의 성능을 .Net Core의 x86 코드 젠 (2.0부터 RyuJit 사용)과 비교하는 것은 흥미로울 것입니다. 이전 jit (일명 Jit32)이 RyuJit이 모르는 몇 가지 트릭을 알고있는 경우가 여전히 있습니다. 그리고 그러한 경우를 발견하면 CoreCLR 리포지토리에서 문제를 해결하십시오.
Andy Ayers

답변:


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 

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

그럼 어디에서 버그를 신고할까요?
Ashkan Nourzadeh

1
.NET 전체 4.7.1에서도 볼 수 있으므로 RyuJIT 버그가 아니라면 모자를 먹을 것입니다.
Jeroen Mostert

2
나는 재현 할 수 없었고 .NET 4.7.1을 설치했고 지금 재현 할 수 있습니다.
user3057557

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, jg 00007FF942690AE7에 있습니다. ebx (루프 끝 값인 4 포함)가 값 i 인 eax보다 크면 (jg) 뒤로 이동합니다. 물론 4이면 실패하므로 배열의 마지막 요소를 쓰지 않습니다.

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


2
언젠가는 어셈블리 / CPU opcode를 배우기 위해 시간을 할애해야합니다. 아마도 순진하게 계속 생각할 것입니다. "메쉬, 나는 IL을 읽고 쓸 수 있습니다-나는 그것을 그릴 수 있어야합니다."-그러나 나는 결코 그것에 대해 다루지 않습니다 :)
Marc Gravell

x64 / x86은 tho로 시작하기에 가장 좋은 어셈블리 언어는 아닙니다.) opcode가 너무 많아서 모든 것을 아는 살아있는 사람이 없다는 것을 읽었습니다. 그것이 사실인지 확실하지 않지만 처음에는 읽기가 쉽지 않습니다. []와 같은 몇 가지 간단한 규칙을 사용하지만 소스 부분 이전의 대상 및 이러한 레지스터가 모두 의미하는 바 (al은 rax의 8 비트 부분, eax는 rax의 32 비트 부분 등)입니다. 필수 사항을 가르쳐야하는 vs tho에서 단계별로 진행할 수 있습니다. 나는 확실히 당신이 이미 IL의 옵 코드를 알고 빨리 데리러 해요)
프랑스어 보우마에게
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.