Perché il comportamento del codice è diverso in modalità di rilascio e debug?


84

Considera il codice seguente:

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

Il risultato in modalità Debug è: 100,100,100,100,100. Ma in modalità di rilascio è: 100,100,100,100,0.

Che cosa sta succedendo?

È stato testato utilizzando .NET framework 4.7.1 e .NET Core 2.0.0.


Quale versione di Visual Studio (o del compilatore) usi?
Styxxy

9
Repro; l'aggiunta di un Console.WriteLine(i);nel loop finale ( dd[i] = d;) lo "risolve", il che suggerisce un bug del compilatore o un bug JIT; guardando
nell'IL

@Styxxy, testato su vs2015, 2017 e mirato a ogni framework .net> = 4.5
Ashkan Nourzadeh

Sicuramente un bug. Scompare anche se rimuovi if (dd.Length >= N) return;, che potrebbe essere una riproduzione più semplice.
Jeroen Mostert

1
Non sorprende che una volta che il confronto è da mele a mele, x64 codegen per .Net Framework e .Net Core abbia prestazioni simili, poiché (per impostazione predefinita) è essenzialmente lo stesso codice che genera jit. Sarebbe interessante confrontare le prestazioni di .Net Framework x86 codegen con .Net Core x86 codegen (che utilizza RyuJit dalla 2.0). Ci sono ancora casi là fuori in cui il vecchio jit (aka Jit32) conosce alcuni trucchi che RyuJit non conosce. E se trovi casi di questo tipo, assicurati di aprire i loro problemi sul repository CoreCLR.
Andy Ayers

Risposte:


70

Questo sembra essere un bug JIT; Ho provato con:

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

e aggiungendo le Console.WriteLine(i)correzioni. L'unico cambiamento di 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 

che sembra esattamente corretto (l'unica differenza è l'extra ldloc.3e call void [System.Console]System.Console::WriteLine(int32), e un obiettivo diverso ma equivalente per br.s).

Avrà bisogno di una correzione JIT, sospetto.

Ambiente:

  • Environment.Version: 4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS: 15.5.0 Anteprima 5.0
  • dotnet --version: 2.1.1

Allora dove segnalare il bug?
Ashkan Nourzadeh

1
Lo vedo anche su .NET full 4.7.1, quindi se questo non è un bug di RyuJIT mi mangio il cappello.
Jeroen Mostert

2
Non sono riuscito a riprodurre, ho installato .NET 4.7.1 e ora posso riprodurre.
user3057557

3
@MarcGravell .Net framework 4.7.1 e .net Core 2.0.0
Ashkan Nourzadeh

4
@AshkanNourzadeh Probabilmente lo registrerei qui per essere onesto, sottolineando che le persone credono che sia un errore RyuJIT
Marc Gravell

6

È davvero un errore di assemblaggio. x64, .net 4.7.1, versione build.

smontaggio:

            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  

Il problema è all'indirizzo 00007FF942690AFD, jg 00007FF942690AE7. Torna indietro se ebx (che contiene 4, il valore di fine ciclo) è maggiore (jg) di eax, il valore i. Questo fallisce ovviamente quando è 4, quindi non scrive l'ultimo elemento nell'array.

Non riesce, perché inc è il valore di registro i (eax, a 0x00007FF942690AF9), quindi lo controlla con 4, ma deve ancora scrivere quel valore. È un po 'difficile individuare dove si trova esattamente il problema, poiché sembra che potrebbe essere il risultato dell'ottimizzazione di (N-Old.Length), poiché la build di debug contiene quel codice, ma la build di rilascio lo calcola in anticipo. Quindi è compito delle persone jit aggiustarlo;)


2
Uno di questi giorni ho bisogno di ritagliarmi un po 'di tempo per imparare i codici operativi assembly / CPU. Forse ingenuamente continuo a pensare "meh, posso leggere e scrivere IL - dovrei essere in grado di capirlo" - ma non ci riesco mai :)
Marc Gravell

x64 / x86 non è il miglior linguaggio assembly per iniziare con tho;) Ha così tanti codici operativi, una volta ho letto che non c'è nessuno al mondo che li conosca tutti. Non sono sicuro che sia vero, ma all'inizio non è così facile da leggere. Anche se utilizza alcune semplici convenzioni, come [], la destinazione prima della parte sorgente e il significato di questi registri (al è una parte a 8 bit di rax, eax è una parte a 32 bit di rax ecc.). Puoi esaminarlo in vs tho, che dovrebbe insegnarti l'essenziale. Sono sicuro che lo prendi rapidamente perché conosci già i codici operativi di IL;)
Frans Bouma
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.