podwójnie? = podwójne? + podwójne?

79

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?

Pan
źródło
15
Jest to istotne dla moich zainteresowań. Teraz musimy tylko poczekać na pana Skeeta.
Mike G
3
Czy może to być debugger? Spróbuj umieścić Console.WriteLine () na końcu i zobacz, co wypisze.
siride
3
Wygląda to na odroczone wykonanie, którego debugger nie widzi poza nią. Prawdopodobnie przypisanie nie zostanie dokonane, dopóki nie będzie konieczne.
Joel Etherton
5
@igrimpe Najwyraźniej 1 + 2 = 25 dla bardzo dużych wartości 1 i 2?
Chris Sinclair
2
Dzięki za odpowiedź wszystkich. Całkiem fajnie jest widzieć, że społeczność tak szybko się tym zajmuje. Zachowanie kompilatora x64 sprawia, że ​​debugowanie tego typu instrukcji jest moim zdaniem nieco nieintuicyjne, ale przynajmniej wydaje się, że wykonanie kodu działa poprawnie w ogólnym schemacie samej aplikacji.
PanE

Odpowiedzi:

86

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ż xnie 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 xjest 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 nim xró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 Mainmetody, zarówno w oknie zegarka, jak i oknie lokalnych, zobaczyłem, że xtak null. 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ę xz 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 + zlinią. 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 xsą równe 3.0D. Jeśli jednak krok za pomocą kodu, można zauważyć, że VS2010 nie pokazuje xjako przydzielony aż po już przeszedł przez Console.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 xzmienna 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 Mainmetody w stosie wywołań. Dbamy o adres IP (wskaźnik instrukcji).

0: 009>! Clrstack
Identyfikator wątku systemu operacyjnego: 0x135c (9)
Witryna wywołań IP dziecka SP
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main (System.String [])
[I tak dalej...]

Jeśli Mainprzejrzymy natywny kod maszynowy metody, możemy zobaczyć, jakie instrukcje zostały uruchomione w momencie, gdy VS przerywa wykonanie:

000007ff`00173388 e813fe25f2 zadzwoń do 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 dodaj rsp, 0D8h
000007ff`001733d6 c3 ret

Korzystanie aktualny adres IP, który dostaliśmy od !clrstackw Main, widzimy, że wykonanie zostało zawieszone na zlecenie bezpośrednio po wywołaniu System.Nullable<double>„s konstruktora. ( int 3jest przerwaniem używanym przez debuggery do zatrzymania wykonywania). Otoczyłem tę linię znakami * i możesz również dopasować linię do L_0056w 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, zanim xzmienna zostanie przypisana przez kod natywny.

EDYCJA: W x64 int 3instrukcja 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.

Christopher Currens
źródło
6
@ChristopherCurrens W porządku stary, zdobyłeś zielony znacznik wyboru. To ma sens. Wyślę coś do Microsoftu. Dziękuję wszystkim za uwagę. Jestem pod wrażeniem wysiłków społeczności.
Pan E
30

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 xnigdy 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:

Console.WriteLine(x);

Zauważysz, że nie tylko wypisuje poprawną wartość polecenia 3get, ale także zmienna xwyświetli poprawną wartość w debugerze (edytuj) po Console.WriteLinewywoł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.

Douglas
źródło
Właśnie to przetestowałem, bo to też była moja pierwsza myśl, a ta odpowiedź jest właściwie niepoprawna. Debuger faktycznie wyświetla wartość null po przypisaniu do czasu zakończenia wywołania Console.WriteLine().
tomfanning
@tomfanning: To nie unieważnia odpowiedzi. Kompilator wydaje instrukcję wykonania przypisania tylko w miejscu, w którym stwierdzi, że będzie to potrzebne - w pliku Console.WriteLine.
Douglas
1
@leppie Kompilator jest (a przynajmniej był) inny. Na przykład będzie wbudowany znacznie agresywniej niż kompilator x86.
Konrad Rudolph
2
@leppie: „ Array Bounds Check Elimination in the CLR ” podaje przykłady, w których kompilatory mogą faktycznie powodować różnice semantyczne. „Kompilatory JIT dla x86 i x64 to obecnie całkiem różne podstawy kodu […] JIT x86 jest szybszy pod względem szybkości kompilacji; JIT x64 jest wolniejszy, ale wykonuje bardziej interesujące optymalizacje ”.
Douglas
2
@MrE - to nie powinno być oznaczone jako poprawna odpowiedź. Gdyby kompilator JIT optymalizował martwy kod, Visual Studio w ogóle nie byłoby w stanie rozwiązać tej xzmiennej. 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.
Christopher Currens