Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

double? = double? + double?

Tags:

c#

.net

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?

like image 234
MrE Avatar asked Nov 30 '12 15:11

MrE


2 Answers

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.

like image 116
Christopher Currens Avatar answered Oct 29 '22 21:10

Christopher Currens


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.

like image 34
Douglas Avatar answered Oct 29 '22 21:10

Douglas