双? =双倍? +双?

时间:2012-11-30 15:41:41

标签: c# .net

我想ping StackOverflow社区,看看我是否因为这个简单的C#代码而失去理智。

我正在开发Windows 7,在.NET 4.0,x64 Debug中构建它。

我有以下代码:

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

    double? x;
    x = y + z;
}

如果我调试并在结束花括号上放置一个断点,我希望Watch Window和Immediate Window中的x = 3。 x = null而不是。

如果我在x86中调试,似乎工作正常。 x64编译器有问题或者我有什么问题吗?

2 个答案:

答案 0 :(得分:86)

道格拉斯对JIT优化死代码的回答是正确的(两者 x86和x64编译器都会这样做)。但是,如果JIT编译器正在优化死代码,那么它将立即显而易见,因为x甚至不会出现在Locals窗口中。此外,手表和即时窗口在尝试访问时会出错:“当前上下文中不存在名称'x'”。这不是你所描述的那样。

您所看到的实际上是Visual Studio 2010中的错误。

首先,我尝试在我的主机上重现此问题:Win7x64和VS2012。对于.NET 4.0目标,x在结束大括号中断时等于3.0D。我决定尝试使用.NET 3.5目标,并且x也设置为3.0D,而不是null。

由于我在.NET 4.0上安装了.NET 4.5,因此无法对此问题进行完美再现,因此我将虚拟机旋转并在其上安装了VS2010。

在这里,我能够重现这个问题。在Main方法的结束花括号上有一个断点,在监视窗口和本地窗口中,我看到xnull。这是它开始变得有趣的地方。我的目标是v2.0运行时,发现它也是null。当然,情况并非如此,因为我在其他计算机上使用相同版本的.NET 2.0运行时成功显示x的值为3.0D

那么,发生了什么呢?在windbg中进行了一些挖掘之后,我发现了这个问题:

VS2010会在实际分配之前显示x的值

我知道这不是它的样子,因为指令指针超过了x = y + z行。您可以通过向方法添加几行代码来自行测试:

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

double? x;
x = y + z;

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

在最后一个花括号上有一个断点,本地和观察窗口显示x等于3.0D。但是,如果您单步执行代码,您会注意到VS2010在之后您已逐步完成x之前未显示Console.WriteLine()

我不知道是否曾向Microsoft Connect报告此错误,但您可能希望这样做,以此代码为例。它显然已经在VS2012中修复了,所以我不确定是否会有更新来解决这个问题。


以下是JIT和VS2010实际发生的事情

使用原始代码,我们可以看到VS正在做什么以及为什么它是错误的。我们还可以看到x变量没有得到优化(除非您已经标记了要在已启用优化的情况下编译的程序集)。

首先,让我们看一下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)

这是调试模式下的正常输出。 Visual Studio定义在赋值期间使用的重复局部变量,然后添加额外的IL命令以将其从CS *变量复制到其各自的用户定义的局部变量。以下是显示这种情况的相应IL代码:

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

让我们用WinDbg进行更深入的调试:

如果在VS2010中调试应用程序并在方法结束时留下断点,我们可以在非侵入模式下轻松附加WinDbg。

以下是调用堆栈中Main方法的框架。我们关心IP(指令指针)。

0:009> !clrstack
OS Thread Id: 0x135c (9)
Child SP         IP               Call Site
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main(System.String[])
[And so on...]

如果我们查看Main方法的本机机器代码,我们可以看到在VS中断执行时已经运行了哪些指令:

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

使用我们从!clrstack Main获得的当前IP,我们看到调用System.Nullable<double>后,指令暂停执行的构造函数。 (int 3是调试器用来停止执行的中断)我用*来包围该行,你也可以将该行与IL中的L_0056匹配。

后面的x64程序集实际上将它分配给局部变量x。我们的指令指针尚未执行该代码,因此VS2010在本机代码分配x变量之前过早地中断。

编辑:在x64中,int 3指令放在分配代码之前,如上所示。在x86中,该指令放在赋值代码之后。这就解释了为什么VS仅在x64中提前破解。如果这是Visual Studio或JIT编译器的错误,很难说。我不确定哪个应用程序插入了断点挂钩。

答案 1 :(得分:30)

众所周知,x64 JIT编译器的优化比x86更具攻击性。 (对于x86和x64编译器生成语义不同的代码的情况,可以参考“Array Bounds Check Elimination in the CLR”。)

在这种情况下,x64编译器检测到x永远不会被读取,并完全取消其赋值;这在编译器优化中称为dead code elimination。要防止这种情况发生,只需在分配后添加以下行:

Console.WriteLine(x);

您将观察到3的正确值不仅会打印出来,而且变量x也会在调试器中显示正确的值 (编辑)< / em> 在引用它的Console.WriteLine调用之后。

编辑:Christopher Currens提供alternative explanation指向Visual Studio 2010中的错误,该错误可能比上述更准确。