Dlaczego zachowanie kodu jest inne w trybie wydawania i debugowania?

84

Rozważ następujący kod:

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

Wynik w trybie debugowania to: 100,100,100,100,100. Ale w trybie wydania jest to: 100,100,100,100,0.

Co się dzieje?

Został przetestowany przy użyciu .NET Framework 4.7.1 i .NET Core 2.0.0.

Ashkan Nourzadeh
źródło
Której wersji programu Visual Studio (lub kompilatora) używasz?
Styxxy
9
Repro; dodanie a Console.WriteLine(i);do końcowej pętli ( dd[i] = d;) "naprawia" to, co sugeruje błąd kompilatora lub błąd JIT; patrząc na IL ...
Marc Gravell
@Styxxy, testowane na vs2015, 2017 i kierowane na każdą platformę .net> = 4,5
Ashkan Nourzadeh,
Zdecydowanie błąd. Znika również po usunięciu if (dd.Length >= N) return;, co może być prostszą repliką.
Jeroen Mostert
1
Nie jest zaskakujące, że po porównaniu z jabłkami do jabłek, kod x64 dla .Net Framework i .Net Core ma podobną wydajność, ponieważ (domyślnie) jest to zasadniczo ten sam kod generujący jit. Byłoby interesujące porównanie wydajności .Net Framework x86 z kodem .Net Core x86 (który używa RyuJit od 2.0). Nadal istnieją przypadki, w których starszy jit (aka Jit32) zna kilka sztuczek, których RyuJit nie zna. Jeśli znajdziesz takie przypadki, pamiętaj, aby otworzyć dla nich problemy w repozytorium CoreCLR.
Andy Ayers

Odpowiedzi:

70

Wygląda na to, że jest to błąd JIT; Testowałem z:

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

i dodanie Console.WriteLine(i)poprawek. Jedyna zmiana IL to:

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

co wygląda dokładnie dobrze (jedyną różnicą jest dodatkowy ldloc.3i call void [System.Console]System.Console::WriteLine(int32)oraz inny, ale równoważny cel dla br.s).

Podejrzewam, że będzie to wymagało poprawki JIT.

Środowisko:

  • Environment.Version: 4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS: 15.5.0 Wersja zapoznawcza 5.0
  • dotnet --version: 2.1.1
Marc Gravell
źródło
Więc gdzie zgłosić błąd?
Ashkan Nourzadeh
1
Widzę to również na .NET full 4.7.1, więc jeśli to nie jest błąd RyuJIT, zjem kapelusz.
Jeroen Mostert
2
Nie udało mi się odtworzyć, zainstalowałem .NET 4.7.1 i mogę teraz odtwarzać.
user3057557
3
@MarcGravell .Net framework 4.7.1 i .net Core 2.0.0
Ashkan Nourzadeh
4
@AshkanNourzadeh Prawdopodobnie zarejestrowałbym to tutaj, aby być uczciwym, podkreślając, że ludzie uważają, że to błąd RyuJIT
Marc Gravell
6

To rzeczywiście błąd zespołu. x64, .net 4.7.1, kompilacja wydania.

demontaż:

            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  

Problem jest pod adresem 00007FF942690AFD, jg 00007FF942690AE7. Skacze z powrotem, jeśli ebx (który zawiera 4, wartość końcową pętli) jest większy (jg) niż eax, wartość i. To kończy się niepowodzeniem, gdy jest to oczywiście 4, więc nie zapisuje ostatniego elementu w tablicy.

Nie udaje się, ponieważ wartość rejestru inc jest i (eax, na 0x00007FF942690AF9), a następnie sprawdza ją z 4, ale nadal musi zapisać tę wartość. Trochę trudno jest dokładnie określić, gdzie dokładnie znajduje się problem, ponieważ wygląda na to, że może być wynikiem optymalizacji (N-Old.Length), ponieważ kompilacja debugowania zawiera ten kod, ale kompilacja wydania oblicza to wstępnie. Więc to dla jitów do naprawienia;)

Frans Bouma
źródło
2
Któregoś dnia muszę poświęcić trochę czasu na nauczenie się instrukcji dotyczących montażu / procesora. Być może naiwnie wciąż myślę „Eee, umiem czytać i pisać IL - powinienem umieć to pojąć” - ale po prostu nigdy się do tego nie zabieram :)
Marc Gravell
x64 / x86 nie jest najlepszym językiem asemblera, od którego można zacząć;) Ma tak wiele rozkazów, przeczytałem kiedyś, że nie ma nikogo, kto zna je wszystkie. Nie jestem pewien, czy to prawda, ale na początku nie jest łatwo to przeczytać. Chociaż używa kilku prostych konwencji, takich jak [], miejsce docelowe przed częścią źródłową i to, co oznaczają te rejestry (al jest 8-bitową częścią rax, eax jest 32-bitową częścią rax itp.). Możesz przejść przez to w porównaniu z tym, co powinno nauczyć Cię podstaw. Jestem pewien, że odbierzesz to szybko, ponieważ znasz już kody operacyjne IL;)
Frans Bouma