双? =双? +双?

我想ping的StackOverflow社区,看看我是否失去了这个简单的C#代码的头脑。

我正在Windows 7上开发,在.NET 4.0 x64 Debug中进行构build。

我有以下代码:

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

如果我debugging,并在结束大括号上放置一个断点,我期望观察窗口和立即窗口x = 3。 x = null代替。

如果我在x86中debugging,事情似乎工作正常。 x64编译器出了什么问题,或者出了什么问题?

道格拉斯的答案是正确的关于JIT优化死代码(x86和x64编译器都会这样做)。 但是,如果JIT编译器正在优化死代码,它将立即显而易见,因为x甚至不会出现在本地窗口中。 此外,当您尝试访问它时,watch和immediate窗口会给您一个错误:“名称'x'在当前上下文中不存在”。 这不是你所描述的情况。

你所看到的实际上是Visual Studio 2010中的一个错误。

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

由于我不能完美地复制这个问题,因为我已经在.NET 4.0之上安装了.NET 4.5,所以我创build了一个虚拟机并安装了VS2010。

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

那么,发生了什么呢? 在windbg周围挖了一些之后,我发现了这个问题:

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

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

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

我不知道这个bug是否曾经被报告给Microsoft Connect,但是您可能想要这样做,以此代码为例。 VS2012显然已经修复了,所以我不确定是否会有更新来解决这个问题。


这是JIT和VS2010中实际发生的事情

用原来的代码,我们可以看到VS在做什么,为什么这是错的。 我们还可以看到, xvariables没有被优化掉(除非你已经标记了在启用优化的情况下被编译的程序集)。

首先,我们来看一下IL的局部variables定义:

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

这是debugging模式下的正常输出。 Visual Studio定义了在赋值过程中使用的重复局部variables,然后添加额外的IL命令将其从CS *variables复制到它各自的用户定义的局部variables。 下面是显示这种情况的相应的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进行更深入的debugging:

如果您在VS2010中debugging应用程序,并在方法结束时留下断点,我们可以非侵入式地轻松连接WinDbg。

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

 0:009>!clrstack
操作系统线程ID:0x135c(9)
子SP IP呼叫站点
 000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main(System.String [])
 [等等...]

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

 000007ff`00173388 e813fe25f2调用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

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

下面的x64程序集实际上将其分配给局部variablesx 。 我们的指令指针还没有执行这个代码,所以VS2010在本地代码分配了xvariables之前就提前中断了。

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

已知x64 JIT编译器在其优化方面比x86更具侵略性。 (对于x86和x64编译器生成语义不同的代码的情况,可以参考“ CLR中的数组边界检查消除 ”。)

在这种情况下,x64编译器正在检测到x从来没有被读取,并且完全取消了它的赋值。 这在编译器优化中被称为死代码消除 。 为防止发生这种情况,只需在分配后面添加以下行:

 Console.WriteLine(x); 

您将观察到,不仅3的正确值被打印,而且variablesx也会在debugging器中显示正确的值(编辑) ,并引用它的Console.WriteLine调用。

编辑 :克里斯托弗Currens提供了一个替代的解释指向在Visual Studio 2010中的错误,这可能是比上述更准确。