doppelt? = doppelt? + doppelt?

79

Ich wollte die StackOverflow-Community anpingen, um zu sehen, ob ich mit diesem einfachen Stück C # -Code den Verstand verliere oder nicht.

Ich entwickle unter Windows 7 und erstelle dies in .NET 4.0, x64 Debug.

Ich habe folgenden Code:

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

    double? x;
    x = y + z;
}

Wenn ich debugge und einen Haltepunkt auf die geschweifte Endklammer setze, erwarte ich im Überwachungsfenster und im Sofortfenster x = 3. x = null stattdessen.

Wenn ich in x86 debugge, scheinen die Dinge gut zu funktionieren. Stimmt etwas mit dem x64-Compiler nicht oder stimmt etwas mit mir nicht?

MrE
quelle
15
Dies ist für meine Interessen relevant. Jetzt müssen wir nur noch auf Mr. Skeet warten.
MikeTheLiar
3
Könnte es der Debugger sein? Fügen Sie am Ende eine Console.WriteLine () ein und sehen Sie, was gedruckt wird.
Siride
3
Dies sieht aus wie eine verzögerte Ausführung, die der Debugger nicht darüber hinaus sehen kann. Die Zuordnung erfolgt möglicherweise erst, wenn sie vorgenommen werden muss.
Joel Etherton
5
@igrimpe Anscheinend 1 + 2 = 25 für sehr große Werte von 1 und 2?
Chris Sinclair
2
Vielen Dank für die Antwort aller. Ziemlich cool zu sehen, wie die Community so schnell darauf springt. Das Verhalten des x64-Compilers macht das Debuggen dieser Art von Anweisungen meiner Meinung nach etwas unintuitiv, aber zumindest scheint die Codeausführung im großen Schema der Anwendung selbst ordnungsgemäß zu funktionieren.
MrE

Antworten:

86

Douglas 'Antwort bezüglich des JIT-optimierenden toten Codes ist richtig ( sowohl die x86- als auch die x64-Compiler werden dies tun). Wenn der JIT-Compiler jedoch den toten Code optimieren würde, wäre dies sofort offensichtlich, da er xnicht einmal im Fenster "Locals" angezeigt würde. Darüber hinaus würden Sie beim Versuch, darauf zuzugreifen, bei der Überwachung und dem Sofortfenster stattdessen einen Fehler erhalten: "Der Name 'x' existiert im aktuellen Kontext nicht". Das haben Sie nicht als passiert beschrieben.

Was Sie sehen, ist tatsächlich ein Fehler in Visual Studio 2010.

Zuerst habe ich versucht, dieses Problem auf meinem Hauptcomputer zu reproduzieren: Win7x64 und VS2012. xIst für .NET 4.0-Ziele gleich 3.0D, wenn die schließende geschweifte Klammer unterbrochen wird. Ich habe mich entschlossen, auch .NET 3.5-Ziele auszuprobieren, und war damit xauch auf 3.0D eingestellt, nicht auf null.

Da ich dieses Problem nicht perfekt reproduzieren kann, da ich .NET 4.5 auf .NET 4.0 installiert habe, habe ich eine virtuelle Maschine hochgefahren und VS2010 darauf installiert.

Hier konnte ich das Problem reproduzieren. Mit einem Haltepunkt in der schließenden geschweiften Klammer der MainMethode, sowohl im Überwachungsfenster als auch im Fenster der Einheimischen, sah ich, dass dies der Fall xwar null. Hier beginnt es interessant zu werden. Ich habe stattdessen auf die v2.0-Laufzeit abgezielt und festgestellt, dass sie auch dort null ist. Dies kann sicherlich nicht der Fall sein, da ich auf meinem anderen Computer dieselbe Version der .NET 2.0-Laufzeit habe, die xmit einem Wert von erfolgreich angezeigt wurde 3.0D.

Also, was passiert dann? Nach einigem Stöbern in Windbg fand ich das Problem:

VS2010 zeigt Ihnen den Wert von x an, bevor er tatsächlich zugewiesen wurde .

Ich weiß, dass es nicht so aussieht, da der Anweisungszeiger hinter der x = y + zZeile steht. Sie können dies selbst testen, indem Sie der Methode einige Codezeilen hinzufügen:

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

double? x;
x = y + z;

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

Mit einem Haltepunkt auf der letzten geschweiften Klammer werden die Einheimischen und das Beobachtungsfenster xgleich angezeigt 3.0D. Wenn Sie jedoch Schritt durch den Code, werden Sie feststellen , dass VS2010 zeigt nicht , xwie erst zugewiesen werden , nachdem Sie durch die gestufte haben Console.WriteLine().

Ich weiß nicht, ob dieser Fehler jemals an Microsoft Connect gemeldet wurde, aber Sie können dies anhand dieses Codes als Beispiel tun. Es wurde jedoch eindeutig in VS2012 behoben, daher bin ich mir nicht sicher, ob es ein Update geben wird, um dies zu beheben oder nicht.


Hier ist, was tatsächlich in der JIT und VS2010 passiert

Mit dem Originalcode können wir sehen, was VS tut und warum es falsch ist. Wir können auch sehen, dass die xVariable nicht weg optimiert wird (es sei denn, Sie haben die zu kompilierende Assembly mit aktivierten Optimierungen markiert).

Schauen wir uns zunächst die lokalen Variablendefinitionen der IL an:

.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)

Dies ist eine normale Ausgabe im Debug-Modus. Visual Studio definiert doppelte lokale Variablen, die bei Zuweisungen verwendet werden, und fügt dann zusätzliche IL-Befehle hinzu, um sie von der CS * -Variablen in die jeweilige benutzerdefinierte lokale Variable zu kopieren. Hier ist der entsprechende IL-Code, der dies zeigt:

// 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 

Lassen Sie uns mit WinDbg ein tieferes Debugging durchführen:

Wenn Sie die Anwendung in VS2010 debuggen und am Ende der Methode einen Haltepunkt belassen, können wir WinDbg problemlos im nicht-invasiven Modus anhängen.

Hier ist der Frame für die MainMethode im Aufrufstapel. Wir kümmern uns um die IP (Anweisungszeiger).

0: 009>! Clrstack
OS-Thread-ID: 0x135c (9)
Untergeordnete SP IP-Anrufsite
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main (System.String [])
[Und so weiter...]

Wenn wir den nativen Maschinencode für die MainMethode anzeigen , können wir sehen, welche Anweisungen zum Zeitpunkt der Unterbrechung der Ausführung durch VS ausgeführt wurden:

000007ff`00173388 e813fe25f2 Rufen Sie mscorlib_ni + 0xd431a0 auf 
           (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

Unter Verwendung der aktuellen IP, von der wir !clrstackin erhalten haben Main, sehen wir, dass die Ausführung der Anweisung direkt nach dem Aufruf des System.Nullable<double>Konstruktors angehalten wurde . ( int 3ist der Interrupt, der von Debuggern verwendet wird, um die Ausführung zu stoppen.) Ich habe diese Zeile mit * umgeben, und Sie können die Zeile auch L_0056in der IL abgleichen.

Die folgende x64-Assembly weist sie tatsächlich der lokalen Variablen zu x. Unser Anweisungszeiger hat diesen Code noch nicht ausgeführt, sodass VS2010 vorzeitig unterbrochen wird, bevor die xVariable vom nativen Code zugewiesen wurde.

BEARBEITEN: In x64 wird die int 3Anweisung vor dem Zuweisungscode platziert, wie Sie oben sehen können. In x86 wird diese Anweisung nach dem Zuweisungscode platziert. Das erklärt, warum VS nur in x64 früh bricht. Es ist schwer zu sagen, ob dies an Visual Studio oder dem JIT-Compiler liegt. Ich bin nicht sicher, welche Anwendung Haltepunkt-Hooks einfügt.

Christopher Currens
quelle
6
@ChristopherCurrens Okay Mann, du hast das grüne Häkchen verdient. Das macht Sinn. Ich werde etwas an Microsoft senden. Vielen Dank für die Aufmerksamkeit aller. Sehr beeindruckt von den Bemühungen der Community.
MrE
30

Der x64-JIT-Compiler ist bekanntermaßen aggressiver in seinen Optimierungen als der x86. (Unter „ Array Bounds Check Elimination in der CLR “ finden Sie einen Fall, in dem die x86- und x64-Compiler semantisch unterschiedlichen Code generieren.)

In diesem Fall erkennt der x64-Compiler, dass er xnie gelesen wird, und beseitigt seine Zuweisung vollständig. Dies wird bei der Compileroptimierung als Dead Code Elimination bezeichnet . Um dies zu verhindern, fügen Sie direkt nach der Zuweisung die folgende Zeile hinzu:

Console.WriteLine(x);

Sie werden feststellen, dass nicht nur der korrekte Wert von 3get gedruckt wird, sondern dass die Variable nach dem Aufruf, der darauf verweist, auch xden korrekten Wert im Debugger anzeigt (bearbeiten)Console.WriteLine .

Bearbeiten : Christopher Currens bietet eine alternative Erklärung, die auf einen Fehler in Visual Studio 2010 hinweist, der möglicherweise genauer ist als der oben genannte.

Douglas
quelle
Ähm, ich habe das gerade getestet, da dies auch mein erster Gedanke war und diese Antwort tatsächlich falsch ist. Der Debugger zeigt nach der Zuweisung bis nach dem Aufruf von tatsächlich null an Console.WriteLine().
Tomfanning
@tomfanning: Das macht die Antwort nicht ungültig. Der Compiler gibt die Anweisung zum Ausführen der Zuweisung nur an dem Punkt aus, an dem er feststellt, dass dies erforderlich ist - am Console.WriteLine.
Douglas
1
@leppie Der Compiler ist (oder war zumindest früher) anders. Zum Beispiel wird es viel aggressiver inline als der x86-Compiler.
Konrad Rudolph
2
@leppie: " Array Bounds Check Elimination in der CLR " enthält Beispiele dafür, wo die Compiler tatsächlich zu semantischen Unterschieden führen können. „Die JIT-Compiler für x86 und x64 sind derzeit sehr unterschiedliche Codebasen […] Die x86-JIT ist in Bezug auf die Kompilierungsgeschwindigkeit schneller. Die x64-JIT ist langsamer, führt jedoch interessantere Optimierungen durch. “
Douglas
2
@MrE - Dies sollte nicht als die richtige Antwort markiert werden. Wenn der JIT-Compiler den toten Code wegoptimieren würde, könnte Visual Studio die xVariable überhaupt nicht auflösen . Es würde nicht im Fenster der Einheimischen angezeigt, und wenn Sie versuchen, es in der Uhr oder in den unmittelbaren Fenstern anzuzeigen, wird die folgende Meldung angezeigt : The name 'x' does not exist in the current context.
Christopher Currens