2010-01-13 11 views
381

Il codice seguente fornisce output diversi durante l'esecuzione del rilascio in Visual Studio e l'esecuzione del rilascio all'esterno di Visual Studio. Sto usando Visual Studio 2008 e il targeting .NET 3.5. Ho anche provato .NET 3.5 SP1.Errore potenziale JIT .NET?

Quando si esegue all'esterno di Visual Studio, il JIT dovrebbe dare il via. O (a) c'è qualcosa di sottile in C# che mi manca o (b) il JIT è effettivamente in errore. Sono dubbioso che il JIT può sbagliare, ma io sono a corto di altri possiblities ...

uscita durante l'esecuzione all'interno di Visual Studio:

0 0, 
    0 1, 
    1 0, 
    1 1, 

uscita durante l'esecuzione di rilascio al di fuori di Visual Studio:

0 2, 
    0 2, 
    1 2, 
    1 2, 

Qual è il motivo?

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 

namespace Test 
{ 
    struct IntVec 
    { 
     public int x; 
     public int y; 
    } 

    interface IDoSomething 
    { 
     void Do(IntVec o); 
    } 

    class DoSomething : IDoSomething 
    { 
     public void Do(IntVec o) 
     { 
      Console.WriteLine(o.x.ToString() + " " + o.y.ToString()+","); 
     } 
    } 

    class Program 
    { 
     static void Test(IDoSomething oDoesSomething) 
     { 
      IntVec oVec = new IntVec(); 
      for (oVec.x = 0; oVec.x < 2; oVec.x++) 
      { 
       for (oVec.y = 0; oVec.y < 2; oVec.y++) 
       { 
        oDoesSomething.Do(oVec); 
       } 
      } 
     } 

     static void Main(string[] args) 
     { 
      Test(new DoSomething()); 
      Console.ReadLine(); 
     } 
    } 
} 
+14

+1 - che piccolo problema questo è :) –

+8

Sì, che ne dici: trovare un bug serio in qualcosa di così essenziale come .Net JIT - congratulazioni! –

+73

Questo sembra essere riprodotto nella mia 9a build del framework 4.0 su x86. Lo passerò alla squadra di jitter. Grazie! –

risposta

202

Si tratta di un bug JIT ottimizzatore. E 'srotolando il ciclo interno, ma non aggiornare il valore oVec.y correttamente:

 for (oVec.x = 0; oVec.x < 2; oVec.x++) { 
0000000a xor   esi,esi       ; oVec.x = 0 
     for (oVec.y = 0; oVec.y < 2; oVec.y++) { 
0000000c mov   edi,2       ; oVec.y = 2, WRONG! 
      oDoesSomething.Do(oVec); 
00000011 push  edi 
00000012 push  esi 
00000013 mov   ecx,ebx 
00000015 call  dword ptr ds:[00170210h]  ; first unrolled call 
0000001b push  edi        ; WRONG! does not increment oVec.y 
0000001c push  esi 
0000001d mov   ecx,ebx 
0000001f call  dword ptr ds:[00170210h]  ; second unrolled call 
     for (oVec.x = 0; oVec.x < 2; oVec.x++) { 
00000025 inc   esi 
00000026 cmp   esi,2 
00000029 jl   0000000C 

Il bug scompare quando si lascia oVec.y incremento di 4, che è troppo molte chiamate per srotolare.

Una soluzione è questa:

for (int x = 0; x < 2; x++) { 
    for (int y = 0; y < 2; y++) { 
     oDoesSomething.Do(new IntVec(x, y)); 
    } 
    } 

UPDATE: ri-controllato nel mese di agosto 2012, questo bug è stato corretto nella versione 4.0.30319 jitter. Ma è ancora presente nel jitter v2.0.50727. Sembra improbabile che risolveranno questo nella vecchia versione dopo così tanto tempo.

+11

+1 Questo è sicuramente un bug. Ho postato più o meno la stessa risposta di una modifica alla mia risposta mentre questa è arrivata. Sembra che rispondere alle domande qui sia spesso una gara ... –

+3

+1, sicuramente un bug - Potrei aver identificato le condizioni per l'errore (non dicendo che nobugz l'ha trovato per causa mia, però!), ma questo (e il tuo, Nick, anche +1 per te) dimostra che la JIT è il colpevole. interessante che l'ottimizzazione sia rimossa o diversa quando IntVec è dichiarato come classe. Anche se si inizializzano esplicitamente i campi struct su 0 prima del ciclo, viene visualizzato lo stesso comportamento. Nasty! –

+3

@Hans Passant Che strumento hai usato per emettere il codice assembly? –

22

Ho copiato il codice in una nuova app di console.

  • debug
    • uscita corretta sia con debugger e senza debugger
  • passati a rilasciare costruire
    • Anche in questo caso, di uscita corretto entrambe le volte
  • Creato un nuova configurazione x86 (sono su runnin g X64 Windows 2008 e stava usando 'Qualsiasi CPU')
  • debug
    • Got l'uscita corretta sia F5 e CTRL + F5
  • uscita Costruire
    • inseguimento dell'uscita con debugger
    • Nessun debugger - Ottenuto l'output errato

Quindi è il JIT x86 che genera in modo errato il codice. Ho cancellato il mio testo originale sul riordino dei loop ecc. Alcune altre risposte qui hanno confermato che il JIT sta srotolando il ciclo in modo non corretto quando su x86.

Per risolvere il problema è possibile modificare la dichiarazione di IntVec in una classe e funziona in tutti i gusti.

pensa che questo ha bisogno di andare su MS Connect ....

-1 a Microsoft!

+1

Idea interessante, ma sicuramente non è "ottimizzazione" ma un bug molto grave nel compilatore se questo è il caso? Sarebbe stato trovato ormai non sarebbe? –

+0

Sono d'accordo con te. Il riordino di loop come questo potrebbe causare problemi inattesi. In realtà questo sembra ancora meno probabile, perché i loop for non possono mai raggiungere 2. –

+2

Sembra uno di questi cattivi Heisenbug: P – arul

79

Credo che questo sia in un vero bug di compilazione JIT. Vorrei segnalarlo a Microsoft e vedere cosa dicono. È interessante notare che ho trovato che il JIT x64 non ha lo stesso problema.

Ecco la mia lettura del JIT x86.

// save context 
00000000 push  ebp 
00000001 mov   ebp,esp 
00000003 push  edi 
00000004 push  esi 
00000005 push  ebx 

// put oDoesSomething pointer in ebx 
00000006 mov   ebx,ecx 

// zero out edi, this will store oVec.y 
00000008 xor   edi,edi 

// zero out esi, this will store oVec.x 
0000000a xor   esi,esi 

// NOTE: the inner loop is unrolled here. 
// set oVec.y to 2 
0000000c mov   edi,2 

// call oDoesSomething.Do(oVec) -- y is always 2!?! 
00000011 push  edi 
00000012 push  esi 
00000013 mov   ecx,ebx 
00000015 call  dword ptr ds:[002F0010h] 

// call oDoesSomething.Do(oVec) -- y is always 2?!?! 
0000001b push  edi 
0000001c push  esi 
0000001d mov   ecx,ebx 
0000001f call  dword ptr ds:[002F0010h] 

// increment oVec.x 
00000025 inc   esi 

// loop back to 0000000C if oVec.x < 2 
00000026 cmp   esi,2 
00000029 jl   0000000C 

// restore context and return 
0000002b pop   ebx 
0000002c pop   esi 
0000002d pop   edi 
0000002e pop   ebp 
0000002f ret  

Questo appare come un'ottimizzazione andato male a me ...