Chciałem wysłać ping do społeczności StackOverflow, aby sprawdzić, czy tracę rozum z tym prostym fragmentem kodu C #.
Rozwijam się na Windows 7, budując to w .NET 4.0, x64 Debug.
Mam następujący kod:
static void Main()
{
double? y = 1D;
double? z = 2D;
double? x;
x = y + z;
}
Jeśli debuguję i umieszczę punkt przerwania w końcowym nawiasie klamrowym, oczekuję, że x = 3 w oknie czujki i oknie bezpośrednim. x = null zamiast tego.
Jeśli debuguję w x86, wydaje się, że wszystko działa dobrze. Czy coś jest nie tak z kompilatorem x64, czy coś jest ze mną nie tak?
Odpowiedzi:
Odpowiedź Douglasa jest prawidłowa, jeśli chodzi o optymalizację martwego kodu JIT ( zrobią to zarówno kompilatory x86, jak i x64). Jednak gdyby kompilator JIT optymalizował martwy kod, byłoby to od razu oczywiste, ponieważ
x
nie pojawiłby się nawet w oknie Locals. Co więcej, okienko obserwacyjne i bezpośrednie wyświetlałyby błąd podczas próby uzyskania do niego dostępu: „Nazwa 'x' nie istnieje w bieżącym kontekście”. To nie jest to, co opisałeś jako się dzieje.To, co widzisz, jest w rzeczywistości błędem w programie Visual Studio 2010.
Najpierw próbowałem odtworzyć ten problem na moim głównym komputerze: Win7x64 i VS2012. W przypadku obiektów docelowych .NET 4.0
x
jest równa 3,0D, gdy przerywa w zamykającym nawiasie klamrowym. Zdecydowałem się również wypróbować cele .NET 3.5, a wraz z nimx
również został ustawiony na 3.0D, a nie null.Ponieważ nie mogę wykonać perfekcyjnej reprodukcji tego problemu, ponieważ mam .NET 4.5 zainstalowane na .NET 4.0, uruchomiłem maszynę wirtualną i zainstalowałem na niej VS2010.
Tutaj udało mi się odtworzyć problem. Z punktem przerwania na zamykającym nawiasie klamrowym
Main
metody, zarówno w oknie zegarka, jak i oknie lokalnych, zobaczyłem, żex
taknull
. Tutaj zaczyna się robić ciekawie. Zamiast tego wybrałem środowisko uruchomieniowe v2.0 i stwierdziłem, że tam też było zerowe. Z pewnością nie może tak być, ponieważ na innym komputerze mam tę samą wersję środowiska uruchomieniowego .NET 2.0, która pomyślnie wyświetlała sięx
z wartością3.0D
.Więc co się dzieje? Po pewnym kopaniu w windbg znalazłem problem:
VS2010 pokazuje wartość x przed faktycznym przypisaniem .
Wiem, że nie tak to wygląda, ponieważ wskaźnik instrukcji znajduje się za
x = y + z
linią. Możesz to sprawdzić samodzielnie, dodając kilka wierszy kodu do metody:double? y = 1D; double? z = 2D; double? x; x = y + z; Console.WriteLine(); // Don't reference x here, still leave it as dead code
Z punktem przerwania na ostatnim nawiasie klamrowym, dane lokalne i okno zegarka
x
są równe3.0D
. Jeśli jednak krok za pomocą kodu, można zauważyć, że VS2010 nie pokazujex
jako przydzielony aż po już przeszedł przezConsole.WriteLine()
.Nie wiem, czy ten błąd został kiedykolwiek zgłoszony do Microsoft Connect, ale możesz to zrobić, używając tego kodu jako przykładu. Wyraźnie zostało to jednak naprawione w VS2012, więc nie jestem pewien, czy będzie aktualizacja, która to naprawi, czy nie.
Oto, co faktycznie dzieje się w JIT i VS2010
Dzięki oryginalnemu kodowi możemy zobaczyć, co robi VS i dlaczego jest źle. Widzimy również, że
x
zmienna nie jest optymalizowana (chyba że zaznaczyłeś zestaw do kompilacji z włączonymi optymalizacjami).Najpierw przyjrzyjmy się definicjom zmiennych lokalnych w 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)
Jest to normalne wyjście w trybie debugowania. Program Visual Studio definiuje zduplikowane zmienne lokalne, które są używane podczas przypisań, a następnie dodaje dodatkowe polecenia IL, aby skopiować je ze zmiennej CS * do odpowiedniej zmiennej lokalnej zdefiniowanej przez użytkownika. Oto odpowiedni kod IL, który pokazuje, że to się dzieje:
// 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
Zróbmy głębsze debugowanie za pomocą WinDbg:
Jeśli debugujesz aplikację w VS2010 i zostawisz punkt przerwania na końcu metody, możemy łatwo dołączyć WinDbg, w trybie nieinwazyjnym.
Oto ramka dla
Main
metody w stosie wywołań. Dbamy o adres IP (wskaźnik instrukcji).Jeśli
Main
przejrzymy natywny kod maszynowy metody, możemy zobaczyć, jakie instrukcje zostały uruchomione w momencie, gdy VS przerywa wykonanie:Korzystanie aktualny adres IP, który dostaliśmy od
!clrstack
wMain
, widzimy, że wykonanie zostało zawieszone na zlecenie bezpośrednio po wywołaniuSystem.Nullable<double>
„s konstruktora. (int 3
jest przerwaniem używanym przez debuggery do zatrzymania wykonywania). Otoczyłem tę linię znakami * i możesz również dopasować linię doL_0056
w IL.Poniższy zestaw x64 faktycznie przypisuje go do zmiennej lokalnej
x
. Nasz wskaźnik instrukcji nie wykonał jeszcze tego kodu, więc VS2010 przedwcześnie się psuje, zanimx
zmienna zostanie przypisana przez kod natywny.EDYCJA: W x64
int 3
instrukcja jest umieszczana przed kodem przydziału, jak widać powyżej. W x86 ta instrukcja jest umieszczana po kodzie przypisania. To wyjaśnia, dlaczego VS psuje się wcześnie tylko w x64. Trudno powiedzieć, czy to wina programu Visual Studio, czy kompilatora JIT. Nie jestem pewien, która aplikacja wstawia punkty przerwania.źródło
Wiadomo, że kompilator x64 JIT jest bardziej agresywny w swoich optymalizacjach niż x86. (Możesz odwołać się do „ Array Bounds Check Elimination in the CLR ” w przypadku przypadku, gdy kompilatory x86 i x64 generują kod, który jest semantycznie inny).
W tym przypadku kompilator x64 wykrywa to, co
x
nigdy nie jest odczytywane i całkowicie eliminuje przypisanie; jest to znane jako eliminacja martwego kodu w optymalizacji kompilatora. Aby temu zapobiec, po prostu dodaj następujący wiersz zaraz po przypisaniu:Zauważysz, że nie tylko wypisuje poprawną wartość polecenia
3
get, ale także zmiennax
wyświetli poprawną wartość w debugerze (edytuj) poConsole.WriteLine
wywołaniu, które się do niej odwołuje.Edycja : Christopher Currens oferuje alternatywne wyjaśnienie wskazujące na błąd w programie Visual Studio 2010, który może być dokładniejszy niż powyższe.
źródło
Console.WriteLine()
.Console.WriteLine
.x
zmiennej. To nie będzie wyświetlane w oknie mieszkańców, i stara się go zobaczyć w zegarku lub natychmiastowe okna dałby komunikat:The name 'x' does not exist in the current context
.