双?=双倍?+双倍?


79

我想对StackOverflow社区进行ping操作,以查看是否因这简单的C#代码而迷失了方向。

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

我有以下代码:

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

    double? x;
    x = y + z;
}

如果我调试并在结尾的花括号上放置一个断点,则期望在“监视窗口”和“立即窗口”中x = 3。x = null。

如果我在x86中调试,一切似乎都可以正常工作。x64编译器有问题还是我有问题?


15
这与我的兴趣有关。现在我们只需要等待斯基特先生。
— Mike G

3
可能是调试器吗?尝试将Console.WriteLine()放在末尾,然后查看打印出来的内容。
— siride 2012年

3
这看起来像是调试器无法看到的延期执行。在需要进行分配之前,可能不会进行分配。
— 乔尔·埃瑟顿

5
@igrimpe显然,如果1和2的值很大,则1 + 2 = 25?
— 克里斯·辛克莱

2
感谢大家的回应。很高兴看到社区如此迅速地加入。我认为x64编译器的行为使调试这种类型的语句有点不直观,但是至少看起来代码执行在应用程序本身的宏伟方案中正常工作。
— MrE 2012年

Answers:


86

道格拉斯答案是对JIT优化死代码(正确都在x86和x64编译器将做到这一点)。但是,如果JIT编译器正在优化x无效代码,则将立即显而易见,因为它甚至不会出现在“本地”窗口中。此外,监视和即时窗口在尝试访问它时会给您一个错误:“名称'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监视窗口和本地窗口中,该方法的右花括号都带有断点,我看到的x是null。这是开始变得有趣的地方。我改为针对v2.0运行时,发现那里也为null。肯定不是这种情况,因为我的另一台计算机上具有相同版本的.NET 2.0运行时,并且成功显示x了值3.0D。

那么,发生了什么呢?经过对windbg的深入研究后,我发现了问题:

VS2010在实际分配x之前向您显示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。但是,如果单步执行代码,则会注意到VS2010x直到单步执行后才显示为已分配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
操作系统线程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添加rsp,0D8h
000007ff`001733d6 c3 ret

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

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

编辑:在x64中,int 3指令位于分配代码之前,如您在上面看到的。在x86中,该指令位于分配代码之后。这就解释了为什么VS只能在x64中提前中断。很难说这是Visual Studio还是JIT编译器的问题。我不确定哪个应用程序插入断点挂钩。


6
@ChristopherCurrens好吧,您赢得了绿色的选中标记。这很有道理。我将发送一些东西到Microsoft。感谢大家对此的关注。社区的努力给我们留下了深刻的印象。
— MrE 2012年

30

众所周知,x64 JIT编译器的优化比x86更积极。(对于x86和x64编译器生成语义上不同的代码,您可以参考“ CLR中的数组边界检查消除”。)

在这种情况下,x64编译器将检测到x永远不会读取的内容,并完全取消其分配。这在编译器优化中称为死代码消除。为防止这种情况发生,只需在分配后添加以下行:

Console.WriteLine(x);

您会发现,不仅3会打印正确的get值,而且变量x也会在引用该调用的调用之后的调试器中(编辑)显示正确的值Console.WriteLine。

编辑:克里斯托弗·柯伦斯(Christopher Currens)提供了另一种解释,指向Visual Studio 2010中的错误,该错误可能比上述错误更准确。


嗯,我刚刚进行了测试,因为这也是我的第一个想法,这个答案实际上是错误的。在分配之后,直到调用之后,调试器实际上才显示null Console.WriteLine()。
— tomfanning 2012年

@tomfanning:这不会使答案无效。编译器仅在检测到需要的时候才发出执行分配的指令Console.WriteLine。
— 道格拉斯

1
@leppie编译器是不同的(或至少以前是不同的)。例如,它将比x86编译器更主动地内联。
— 康拉德·鲁道夫2012年

2
@leppie:“ CLR中的数组边界检查消除”给出了一些示例,说明了编译器实际上可能引起语义差异的地方。“ x86和x64的JIT编译器目前是完全不同的代码库[…] x86 JIT的编译速度更快;x64 JIT速度较慢,但​​进行了更多有趣的优化。”
— 道格拉斯

2
@MrE-不应将其标记为正确答案。如果JIT编译器优化了无效代码,Visual Studio将根本无法解析该x变量。它不会出现在本地窗口中,并且尝试在手表或即时窗口中查看该消息:The name 'x' does not exist in the current context。
— Christopher Currens 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.