Pourquoi le comportement du code est-il différent en mode version et débogage?

84

Considérez le code suivant:

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

Le résultat en mode débogage est: 100,100,100,100,100. Mais en mode Release c'est: 100,100,100,100,0.

Qu'est-ce qui se passe?

Il a été testé avec .NET Framework 4.7.1 et .NET Core 2.0.0.

Ashkan Nourzadeh
la source
Quelle version de Visual Studio (ou du compilateur) utilisez-vous?
Styxxy
9
Repro; l'ajout d'un Console.WriteLine(i);dans la boucle finale ( dd[i] = d;) le "corrige", ce qui suggère un bogue du compilateur ou un bogue JIT; looking into the IL ...
Marc Gravell
@Styxxy, testé sur vs2015, 2017 et ciblé tous les frameworks .net> = 4.5
Ashkan Nourzadeh
Certainement un bug. Il disparaît également si vous supprimez if (dd.Length >= N) return;, ce qui peut être une repro plus simple.
Jeroen Mostert
1
Il n'est pas surprenant qu'une fois que la comparaison porte sur des pommes à des pommes, le codegen x64 pour .Net Framework et .Net Core a des performances similaires, car (par défaut) il s'agit essentiellement du même code de génération de jit. Il serait intéressant de comparer les performances du codegen .Net Framework x86 avec le codegen x86 de .Net Core (qui utilise RyuJit depuis 2.0). Il existe encore des cas où l'ancien jit (alias Jit32) connaît quelques astuces que RyuJit ne connaît pas. Et si vous trouvez de tels cas, assurez-vous de leur ouvrir les problèmes sur le référentiel CoreCLR.
Andy Ayers

Réponses:

70

Cela semble être un bogue JIT; J'ai testé avec:

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

et en ajoutant les Console.WriteLine(i)correctifs. Le seul changement IL est:

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

contre

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

qui semble tout à fait correct (la seule différence est le supplément ldloc.3et call void [System.Console]System.Console::WriteLine(int32), et une cible différente mais équivalente pour br.s).

Il faudra un correctif JIT, je suppose.

Environnement:

  • Environment.Version: 4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS: 15.5.0 Aperçu 5.0
  • dotnet --version: 2.1.1
Marc Gravell
la source
Alors où signaler le bug?
Ashkan Nourzadeh
1
Je le vois également sur .NET full 4.7.1, donc si ce n'est pas un bug RyuJIT, je vais manger mon chapeau.
Jeroen Mostert
2
Je n'ai pas pu reproduire, installé .NET 4.7.1 et je peux reproduire maintenant.
user3057557
3
@MarcGravell .Net framework 4.7.1 et .net Core 2.0.0
Ashkan Nourzadeh
4
@AshkanNourzadeh Je l'enregistrerais probablement ici pour être honnête, en insistant sur le fait que les gens croient que c'est une erreur RyuJIT
Marc Gravell
6

C'est en effet une erreur d'assemblage. x64, .net 4.7.1, version de version.

démontage:

            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  

Le problème se trouve à l'adresse 00007FF942690AFD, le jg 00007FF942690AE7. Il saute en arrière si ebx (qui contient 4, la valeur de fin de boucle) est plus grand (jg) que eax, la valeur i. Cela échoue quand il est 4 bien sûr, donc il n'écrit pas le dernier élément du tableau.

Il échoue, car il incorpore la valeur de registre i (eax, à 0x00007FF942690AF9), puis le vérifie avec 4, mais il doit toujours écrire cette valeur. Il est un peu difficile de localiser exactement le problème, car il semble que cela pourrait être le résultat de l'optimisation de (N-Old.Length), car la version de débogage contient ce code, mais la version de version précalcule cela. C'est donc aux gens de jit de réparer;)

Frans Bouma
la source
2
Un de ces jours, j'ai besoin de prendre du temps pour apprendre les opcodes d'assemblage / CPU. Peut-être naïvement je n'arrête pas de me dire "meh, je sais lire et écrire IL - je devrais être capable de le grok" - mais je n'y arrive jamais :)
Marc Gravell
x64 / x86 n'est pas le meilleur langage d'assemblage pour commencer avec tho;) Il a tellement de opcodes, j'ai lu une fois qu'il n'y a personne vivant qui les connaisse tous. Je ne sais pas si c'est vrai, mais ce n'est pas si facile de le lire au début. Bien qu'il utilise quelques conventions simples, comme le [], la destination avant la partie source et ce que signifient tous ces registres (al fait partie 8 bits de rax, eax fait partie 32 bits de rax, etc.). Vous pouvez le parcourir dans vs tho, qui devrait vous apprendre l'essentiel. Je suis sûr que vous le prenez rapidement car vous connaissez déjà les opcodes IL;)
Frans Bouma