I wanted to ping the StackOverflow community to see whether or not I'm losing my mind with this simple bit of C# code.
I'm developing on Windows 7, building this in .NET 4.0, x64 Debug.
I have the following code:
static void Main()
{
double? y = 1D;
double? z = 2D;
double? x;
x = y + z;
}
If I debug and put a breakpoint on the ending curly brace, I expect x = 3 in the Watch Window and Immediate Window. x = null instead.
If I debug in x86, things seem to work fine. Is something wrong with the x64 compiler or is something wrong with me?
Douglas' answer is correct about the JIT optimizing dead code (both the x86 and x64 compilers will do this). However, if the JIT compiler were optimizing the dead code it would be immediately obvious because x
wouldn't even appear in the Locals window. Furthermore, the watch and immediate window would instead give you an error when trying to access it: "The name 'x' does not exist in the current context". That's not what you've described as happening.
What you are seeing is actually a bug in Visual Studio 2010.
First, I tried to reproduce this issue on my main machine: Win7x64 and VS2012. For .NET 4.0 targets, x
is equal to 3.0D when it breaks on the closing curly brace. I decided to try .NET 3.5 targets as well, and with that, x
also was set to 3.0D, not null.
Since I can't do a perfect reproduction of this issue since I have .NET 4.5 installed on top of .NET 4.0, I spun up a virtual machine and installed VS2010 on it.
Here, I was able to reproduce the issue. With a breakpoint on the closing curly bracket of the Main
method, in both the watch window and the locals window, I saw that x
was null
. This is where it starts to get interesting. I targeted the v2.0 runtime instead and found that it was null there too. Surely that can't be the case since I have the same version of the .NET 2.0 runtime on my other computer that successfully showed x
with a value of 3.0D
.
So, what's happening, then? After some digging around in windbg, I found the issue:
VS2010 is showing you the value of x before it's actually been assigned.
I know that's not what it looks like, since the instruction pointer is past the x = y + z
line. You can test this yourself by adding a few lines of code to the method:
double? y = 1D;
double? z = 2D;
double? x;
x = y + z;
Console.WriteLine(); // Don't reference x here, still leave it as dead code
With a breakpoint on the final curly brace, the locals and watch window shows x
as equal to 3.0D
. However, if you step through the code, you'll notice that VS2010 doesn't show x
as being assigned until after you've stepped through the Console.WriteLine()
.
I don't know if this bug had ever been reported to Microsoft Connect, but you might want to do that, with this code as an example. It's clearly been fixed in VS2012 however, so I'm not sure if there will be an update to fix this or not.
Here's what is actually happening in the JIT and VS2010
With the original code, we can see what VS is doing and why it's wrong. We can also see that the x
variable isn't getting optimized away (unless you've marked the assembly to be compiled with optimizations enabled).
First, let's look at the local variable definitions of the 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)
This is normal output in debug mode. Visual Studio defines duplicate local variables which uses during assignments, and then adds extra IL commands to copy it from the CS* variable to it's respective user-defined local variable. Here is the corresponding IL code that shows this happening:
// 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
Let's do some deeper debugging with WinDbg:
If you debug the application in VS2010 and leave a breakpoint at the end of the method, we can attach WinDbg easily, in non-invasive mode.
Here is the frame for the Main
method in the call stack. We care about the IP (instruction pointer).
0:009> !clrstack OS Thread Id: 0x135c (9) Child SP IP Call Site 000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main(System.String[]) [And so on...]
If we view the native machine code for the Main
method, we can see what instructions have been run at the time that VS breaks execution:
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
Using the current IP that we got from !clrstack
in Main
, we see that execution was suspended on the instruction directly after the call to System.Nullable<double>
's constructor. (int 3
is the interrupt used by debuggers to stop execution) I've surrounded that line with *'s, and you can also match up the line to L_0056
in the IL.
The x64 assembly that follows actually assigns it to the local variable x
. Our instruction pointer hasn't executed that code yet, so VS2010 is prematurely breaking before the x
variable has been assigned by the native code.
EDIT: In x64, the int 3
instruction is placed before the assignment code, as you can see above. In x86, that instruction is placed after the assignment code. That explains why VS is breaking early only in x64. It's tough to say if this is the fault of Visual Studio or the JIT compiler. I'm unsure which application inserts breakpoint hooks.
The x64 JIT compiler is known to be more aggressive in its optimizations than the x86. (You can refer to “Array Bounds Check Elimination in the CLR” for a case where the x86 and x64 compilers generate code that is semantically different.)
In this case, the x64 compiler is detecting that x
never gets read, and does away with its assignment altogether; this is known as dead code elimination in compiler optimization. To prevent this from happening, just add the following line right after the assignment:
Console.WriteLine(x);
You will observe that not only does the correct value of 3
get printed, but the variable x
would display the correct value in the debugger as well (edit) following the Console.WriteLine
call which references it.
Edit: Christopher Currens offers an alternative explanation pointing towards a bug in Visual Studio 2010, which might be more accurate than the above.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With