Warum unterscheidet sich das Codeverhalten im Release- und Debug-Modus?

84

Betrachten Sie den folgenden Code:

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

Das Ergebnis im Debug-Modus ist: 100.100.100.100.100. Im Release-Modus ist dies jedoch: 100.100.100.100,0.

Was ist los?

Es wurde mit .NET Framework 4.7.1 und .NET Core 2.0.0 getestet.

Ashkan Nourzadeh
quelle
Welche Version von Visual Studio (oder den Compiler) verwenden Sie?
Styxxy
9
Repro; Das Hinzufügen eines Console.WriteLine(i);in die letzte Schleife ( dd[i] = d;) "behebt" es, was auf einen Compiler- oder JIT-Fehler hindeutet. Blick in die IL ...
Marc Gravell
@Styxxy, getestet am vs2015, 2017 und zielte auf jedes .net Framework> = 4.5
Ashkan Nourzadeh
Auf jeden Fall ein Fehler. Es verschwindet auch, wenn Sie entfernen if (dd.Length >= N) return;, was eine einfachere Reproduktion sein kann.
Jeroen Mostert
1
Es ist nicht überraschend, dass x64-Codegen für .Net Framework und .Net Core nach dem Vergleich von Äpfeln zu Äpfeln eine ähnliche Leistung aufweist, da es sich (standardmäßig) im Wesentlichen um denselben Code handelt, der JIT generiert. Es wäre interessant, die Leistung des x86-Codegens von .Net Framework mit dem x86-Codegen von .Net Core (der RyuJit seit 2.0 verwendet) zu vergleichen. Es gibt immer noch Fälle, in denen der ältere Jit (auch bekannt als Jit32) einige Tricks kennt, die RyuJit nicht kennt. Und wenn Sie solche Fälle finden, stellen Sie bitte sicher, dass Sie Probleme für sie im CoreCLR-Repo öffnen.
Andy Ayers

Antworten:

70

Dies scheint ein JIT-Fehler zu sein. Ich habe getestet mit:

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

und die Console.WriteLine(i)Korrekturen hinzufügen . Die einzige IL-Änderung ist:

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

Das sieht genau richtig aus (der einzige Unterschied ist das Extra ldloc.3und call void [System.Console]System.Console::WriteLine(int32), und ein anderes, aber gleichwertiges Ziel für br.s).

Ich vermute, es wird einen JIT-Fix brauchen.

Umgebung:

  • Environment.Version: 4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS: 15.5.0 Vorschau 5.0
  • dotnet --version: 2.1.1
Marc Gravell
quelle
Wo soll dann der Fehler gemeldet werden?
Ashkan Nourzadeh
1
Ich sehe es auch in .NET Full 4.7.1. Wenn dies also kein RyuJIT-Fehler ist, esse ich meinen Hut.
Jeroen Mostert
2
Ich konnte nicht reproduzieren, installierte .NET 4.7.1 und kann jetzt reproduzieren.
user3057557
3
@MarcGravell .Net Framework 4.7.1 und .net Core 2.0.0
Ashkan Nourzadeh
4
@AshkanNourzadeh Ich würde es wahrscheinlich hier eintragen , um ehrlich zu sein, und betonen, dass die Leute glauben, dass es ein RyuJIT-Fehler ist
Marc Gravell
6

Es ist in der Tat ein Montagefehler. x64, .net 4.7.1, Release Build.

Demontage:

            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  

Das Problem ist unter der Adresse 00007FF942690AFD, der jg 00007FF942690AE7. Es springt zurück, wenn ebx (das 4 enthält, den Endwert der Schleife) größer (jg) ist als eax, der Wert i. Dies schlägt natürlich fehl, wenn es 4 ist, sodass nicht das letzte Element in das Array geschrieben wird.

Es schlägt fehl, weil es den Registerwert von i (eax, bei 0x00007FF942690AF9) enthält und es dann mit 4 überprüft, aber es muss diesen Wert noch schreiben. Es ist etwas schwierig zu bestimmen, wo genau sich das Problem befindet, da es möglicherweise das Ergebnis der Optimierung von (N-Old.Length) ist, da der Debug-Build diesen Code enthält, der Release-Build dies jedoch vorberechnet. Das müssen die Leute also reparieren;)

Frans Bouma
quelle
2
Eines Tages muss ich mir etwas Zeit nehmen, um Assembler- / CPU-Opcodes zu lernen. Vielleicht naiv denke ich immer wieder "meh, ich kann IL lesen und schreiben - ich sollte es verstehen können" - aber ich komme einfach nie dazu :)
Marc Gravell
x64 / x86 ist nicht die beste Assemblersprache für den Anfang;) Es hat so viele Opcodes, dass ich einmal gelesen habe, dass niemand lebt, der sie alle kennt. Ich bin mir nicht sicher, ob das stimmt, aber es ist zunächst nicht so einfach, es zu lesen. Obwohl es einige einfache Konventionen verwendet, wie das [], das Ziel vor dem Quellteil und was diese Register alle bedeuten (al ist ein 8-Bit-Teil von Rax, eax ist ein 32-Bit-Teil von Rax usw.). Sie können es in vs tho durchgehen, was Ihnen das Wesentliche beibringen sollte. Ich bin sicher, Sie lernen es schnell, da Sie bereits IL-Opcodes kennen;)
Frans Bouma