2012-11-30 18 views
79

Volevo eseguire il ping della community di StackOverflow per vedere se stavo perdendo la testa con questo semplice bit di codice C#.doppio? = doppio? + doppio?

Sto sviluppando su Windows 7, costruendo questo in .NET 4.0, x64 Debug.

Ho il codice seguente:

static void Main() 
{ 
    double? y = 1D; 
    double? z = 2D; 

    double? x; 
    x = y + z; 
} 

Se il debug e mettere un punto di interruzione il finale parentesi graffa, mi aspetto che x = 3 nella finestra di controllo e finestra immediata. x = null invece.

Se eseguo il debug in x86, le cose sembrano funzionare correttamente. C'è qualcosa di sbagliato nel compilatore x64 o c'è qualcosa di sbagliato in me?

+0

ideone funziona mono e funziona come previsto. http://ideone.com/DbuxwD –

+15

Questo è rilevante per i miei interessi. Ora dobbiamo solo aspettare il signor Skeet. – MikeTheLiar

+3

Potrebbe essere il debugger? Prova a mettere un Console.WriteLine() alla fine e vedere cosa viene stampato. – siride

risposta

85

La risposta di Douglas è corretta in merito al codice guasto di ottimizzazione JIT (sia che i compilatori x86 e x64 lo faranno). Tuttavia, se il compilatore JIT stava ottimizzando il codice morto, sarebbe immediatamente ovvio perché x non comparirebbe nemmeno nella finestra dei locali. Inoltre, l'orologio e la finestra immediata ti darebbero un errore quando proverai ad accedervi: "Il nome 'x' non esiste nel contesto corrente". Non è quello che hai descritto come accadendo.

Quello che state vedendo è in realtà un bug in Visual Studio 2010.

In primo luogo, ho provato a riprodurre il problema sulla mia macchina principale: Win7x64 e VS2012. Per gli obiettivi .NET 4.0, x equivale a 3.0D quando si interrompe sulla parentesi graffa di chiusura. Ho deciso di provare anche i target .NET 3.5 e, con questo, x è stato impostato su 3.0D, non null.

Poiché non riesco a riprodurre perfettamente questo problema da quando ho installato .NET 4.5 su .NET 4.0, ho attivato una macchina virtuale e installato VS2010 su di esso.

Qui, sono stato in grado di riprodurre il problema. Con un punto di interruzione sulla parentesi graffa di chiusura del metodo Main, sia nella finestra di controllo che nella finestra dei locali, ho visto che x era null. Questo è dove inizia a diventare interessante. Ho preso di mira il runtime v2.0 invece e ho scoperto che era nullo anche lì.Sicuramente non può essere il caso poiché ho la stessa versione del runtime .NET 2.0 sul mio altro computer che ha mostrato con successo x con un valore di 3.0D.

Allora, che succede? Dopo aver scavato in windbg, ho riscontrato il problema:

VS2010 mostra il valore di x prima che sia stato assegnato a.

So che non è quello che sembra, dal momento che il puntatore dell'istruzione è passato la linea x = y + z. È possibile verificare questo da soli con l'aggiunta di poche righe di codice al metodo:

double? y = 1D; 
double? z = 2D; 

double? x; 
x = y + z; 

Console.WriteLine(); // Don't reference x here, still leave it as dead code 

Con un punto di interruzione sulla parentesi graffa finale, la gente del posto e la finestra guardare spettacoli x uguale a 3.0D. Tuttavia, se si passa attraverso il codice, si noterà che VS2010 non mostra x come assegnato fino a dopo che si è passati attraverso lo Console.WriteLine().

Non so se questo bug sia mai stato segnalato a Microsoft Connect, ma potresti voler farlo, con questo codice come esempio. Tuttavia, è stato chiaramente risolto in VS2012, quindi non sono sicuro se ci sarà un aggiornamento per risolvere questo problema o meno.


Ecco ciò che sta realmente accadendo nel JIT e VS2010

Con il codice originale, siamo in grado di vedere ciò che VS sta facendo e perché è sbagliato. Possiamo anche vedere che la variabile x non viene ottimizzata (a meno che tu non abbia contrassegnato l'assembly da compilare con le ottimizzazioni abilitate).

In primo luogo, diamo un'occhiata alle definizioni delle variabili locali del IL:

.locals init (
    [0] valuetype [mscorlib]System.Nullable`1<float64> y, 
    [1] valuetype [mscorlib]System.Nullable`1<float64> z, 
    [2] valuetype [mscorlib]System.Nullable`1<float64> x, 
    [3] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0000, 
    [4] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0001, 
    [5] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0002) 

Questo è normale output in modalità debug. Visual Studio definisce le variabili locali duplicate che vengono utilizzate durante le assegnazioni e quindi aggiunge ulteriori comandi IL per copiarle dalla variabile CS * alla rispettiva variabile locale definita dall'utente. Ecco il codice IL corrispondente che mostra che questo accada:

// For the line x = y + z 
L_0045: ldloca.s CS$0$0000 // earlier, y was stloc.3 (CS$0$0000) 
L_0047: call instance !0 [mscorlib]System.Nullable`1<float64>::GetValueOrDefault() 
L_004c: conv.r8   // Convert to a double 
L_004d: ldloca.s CS$0$0001 // earlier, z was stloc.s CS$0$0001 
L_004f: call instance !0 [mscorlib]System.Nullable`1<float64>::GetValueOrDefault() 
L_0054: conv.r8   // Convert to a double 
L_0055: add    // Add them together 
L_0056: newobj instance void [mscorlib]System.Nullable`1<float64>::.ctor(!0) // Create a new nulable 
L_005b: nop    // NOPs are placed in for debugging purposes 
L_005c: stloc.2   // Save the newly created nullable into `x` 
L_005d: ret 

Facciamo un po di debug più profondo con WinDbg:

Se si esegue il debug dell'applicazione in VS2010 e lascia un punto di interruzione alla fine del metodo, possiamo collega WinDbg facilmente, in modalità non invasiva.

Di seguito è riportato il frame per il metodo Main nello stack di chiamate. Ci interessa l'IP (puntatore di istruzioni).

 
0:009> !clrstack 
OS Thread Id: 0x135c (9) 
Child SP   IP    Call Site 
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main(System.String[]) 
[And so on...] 

Se consideriamo il codice macchina nativo per il metodo Main, possiamo vedere quello che le istruzioni sono stati eseguiti nel momento in cui VS rompe esecuzione:

 
000007ff`00173388 e813fe25f2  call mscorlib_ni+0xd431a0 
      (000007fe`f23d31a0) (System.Nullable`1[[System.Double, mscorlib]]..ctor(Double), mdToken: 0000000006001ef2) 
****000007ff`0017338d cc    int  3**** 
000007ff`0017338e 8d8c2490000000 lea  ecx,[rsp+90h] 
000007ff`00173395 488b01   mov  rax,qword ptr [rcx] 
000007ff`00173398 4889842480000000 mov  qword ptr [rsp+80h],rax 
000007ff`001733a0 488b4108  mov  rax,qword ptr [rcx+8] 
000007ff`001733a4 4889842488000000 mov  qword ptr [rsp+88h],rax 
000007ff`001733ac 488d8c2480000000 lea  rcx,[rsp+80h] 
000007ff`001733b4 488b01   mov  rax,qword ptr [rcx] 
000007ff`001733b7 4889442440  mov  qword ptr [rsp+40h],rax 
000007ff`001733bc 488b4108  mov  rax,qword ptr [rcx+8] 
000007ff`001733c0 4889442448  mov  qword ptr [rsp+48h],rax 
000007ff`001733c5 eb00   jmp  000007ff`001733c7 
000007ff`001733c7 0f28b424c0000000 movaps xmm6,xmmword ptr [rsp+0C0h] 
000007ff`001733cf 4881c4d8000000 add  rsp,0D8h 
000007ff`001733d6 c3    ret 

Utilizzando l'IP corrente che abbiamo ottenuto da !clrstack in Main, vediamo che l'esecuzione è stata sospesa sull'istruzione direttamente dopo la chiamata al costruttore System.Nullable<double>.(int 3 è l'interrupt utilizzato dai debugger per interrompere l'esecuzione) Ho circondato quella linea con * e puoi anche abbinare la linea a L_0056 nell'IL.

L'assembly x64 che segue lo assegna effettivamente alla variabile locale x. Il nostro puntatore di istruzioni non ha ancora eseguito quel codice, quindi VS2010 si interrompe prematuramente prima che la variabile x sia stata assegnata dal codice nativo.

MODIFICA: in x64, l'istruzione int 3 viene inserita prima del codice di assegnazione, come si può vedere sopra. In x86, tale istruzione viene inserita dopo il codice di assegnazione. Questo spiega perché VS si sta rompendo presto solo in x64. È difficile dire se è colpa di Visual Studio o del compilatore JIT. Non sono sicuro quale applicazione inserisca i punti di interruzione.

+4

+1: sembra plausibile. Grazie! – Douglas

+6

@ChristopherCurrens Ok, hai guadagnato il segno di spunta verde. Questo ha senso. Invierò qualcosa in Microsoft. Grazie per l'attenzione di tutti a questo. Molto colpito dallo sforzo della comunità. – MrE

30

Il compilatore x64 JIT è noto per essere più aggressivo nelle sue ottimizzazioni rispetto a x86. (È possibile fare riferimento a “Array Bounds Check Elimination in the CLR” per un caso in cui i compilatori x86 e x64 generano codice che è semanticamente diversa.)

In questo caso, il compilatore x64 rileva che x non viene mai letto, e lo fa via con la sua assegnazione del tutto; questo è noto come dead code elimination nell'ottimizzazione del compilatore. Per evitare che ciò accada, è sufficiente aggiungere la seguente riga a destra dopo l'assegnazione:

Console.WriteLine(x); 

Noterete che non solo il valore corretto di 3 vengono stampati, ma la variabile x dovrebbe visualizzare il valore corretto nel debugger pure (modifica) in seguito alla chiamata Console.WriteLine a cui fa riferimento.

Modifica: Christopher Currens offre uno alternative explanation che punta a un errore in Visual Studio 2010, che potrebbe essere più accurato di quanto riportato sopra.

+0

Ehm, ho appena provato questo perché anche questo è stato il mio primo pensiero, e questa risposta è in realtà sbagliata. Il debugger visualizza effettivamente null dopo l'assegnazione fino a dopo la chiamata a 'Console.WriteLine()'. – tomfanning

+0

@tomfanning: ciò non invalida la risposta. Il compilatore sta emettendo le istruzioni per eseguire l'assegnazione solo nel momento in cui rileva che sarà necessario - alla console.WriteLine'. – Douglas

+0

Dubito che il compilatore sia diverso (x86 vs 64-bit). Quello che potrebbe essere visto qui è un ottimizzazione JIT che non è disattivata. – leppie