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.
ideone funziona mono e funziona come previsto. http://ideone.com/DbuxwD –
Questo è rilevante per i miei interessi. Ora dobbiamo solo aspettare il signor Skeet. – MikeTheLiar
Potrebbe essere il debugger? Prova a mettere un Console.WriteLine() alla fine e vedere cosa viene stampato. – siride