双?=双倍?+双倍?


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监视窗口和本地窗口中,该方法的右花括号都带有断点,我看到的xnull。这是开始变得有趣的地方。我改为针对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,我们从得到!clrstackMain,我们看到,执行暂停的指令后直接调用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.