【问题标题】:double? = double? + double?双倍的? = 双倍? + 双倍?
【发布时间】:2012-11-30 15:41:41
【问题描述】:

我想 ping StackOverflow 社区,看看我是否对这段简单的 C# 代码失去了理智。

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

我有以下代码:

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

    double? x;
    x = y + z;
}

如果我调试并在结尾的花括号上放置一个断点,我希望观察窗口和立即窗口中的 x = 3。 x = null 代替。

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

【问题讨论】:

  • 这与我的兴趣有关。现在我们只需要等待斯基特先生。
  • 会不会是调试器?尝试将 Console.WriteLine() 放在最后,看看它会打印什么。
  • 这看起来像是调试器无法看到的延迟执行。在需要进行分配之前,可能不会进行分配。
  • @igrimpe 对于非常大的 1 和 2 值,显然 1 + 2 = 25?
  • 感谢大家的回复。看到社区如此迅速地参与进来真是太酷了。在我看来,x64 编译器的行为使得调试这种类型的语句有点不直观,但至少看起来代码执行在应用程序本身的宏大方案中可以正常工作。

标签: c# .net


【解决方案1】:

Douglas 对 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 运行时,发现它在那里也为空。当然不可能,因为我在另一台计算机上拥有相同版本的 .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。但是,如果您单步执行代码,您会注意到 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 操作系统线程 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

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

随后的 x64 程序集实际上将其分配给局部变量 x。我们的指令指针还没有执行该代码,所以 VS2010 在本地代码分配 x 变量之前就提前中断了。

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

【讨论】:

  • @ChristopherCurrens 好的,你赢得了绿色复选标记。这是有道理的。我会向微软发送一些东西。感谢大家对此的关注。社区的努力给我们留下了深刻的印象。
【解决方案2】:

众所周知,x64 JIT 编译器在优化方面比 x86 更积极。 (关于 x86 和 x64 编译器生成的代码在语义上不同的情况,您可以参考“Array Bounds Check Elimination in the CLR”。)

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

Console.WriteLine(x);

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

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

【讨论】:

  • 呃,我刚刚测试了这个,因为这也是我的第一个想法,这个答案实际上是不正确的。在调用 Console.WriteLine() 之前,调试器实际上在赋值之后显示 null。
  • @tomfanning:这并不会使答案无效。编译器仅在它检测​​到需要执行赋值时发出指令——在Console.WriteLine
  • @leppie 编译器(或至少,曾经是)不同。例如,它会比 x86 编译器更积极地内联。
  • @leppie: “Array Bounds Check Elimination in the CLR” 给出了编译器实际上可能导致语义差异的示例。 “x86 和 x64 的 JIT 编译器目前是完全不同的代码库 [...] x86 JIT 在编译速度方面更快; x64 JIT 速度较慢,但​​进行了更有趣的优化。”
  • @MrE - 这不应该被标记为正确答案。如果 JIT 编译器正在优化死代码,Visual Studio 将根本无法解析 x 变量。它不会出现在本地窗口中,尝试在手表或即时窗口中查看它会显示消息:The name 'x' does not exist in the current context
猜你喜欢
  • 2014-02-28
  • 2012-10-31
  • 2013-03-30
  • 1970-01-01
  • 2013-05-07
  • 2014-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多