JIT是否生成了错误的代码?

16

我一直在查看一些代码,但有一部分无法正常工作。除了下面这行代码之外,其他都看起来没问题。

Transport = Transport?? MockITransportUtil.GetMock(true);

在执行那行代码之前,Transport是空的。我看到GetMock被执行并且返回了一个非空对象。但在那行代码之后,Transport仍然为空。
我查看了生成的IL代码,觉得没问题。
 IL_0002:  ldarg.0
  IL_0003:  ldfld      class [Moq]Moq.Mock`1<class [CommLibNet]CommLibNET.ITransport> Curex.Services.Common.UnitTests.Messaging.TestIGuaranteedSubscriptionBase::Transport
  IL_0008:  dup
  IL_0009:  brtrue.s   IL_0012
  IL_000b:  pop
  IL_000c:  ldc.i4.1
  IL_000d:  call       class [Moq]Moq.Mock`1<class [CommLibNet]CommLibNET.ITransport> Curex.Services.Common.UnitTests.Mocking.MockITransportUtil::GetMock(bool)
  IL_0012:  stfld      class [Moq]Moq.Mock`1<class [CommLibNet]CommLibNET.ITransport> Curex.Services.Common.UnitTests.Messaging.TestIGuaranteedSubscriptionBase::Transport

我们看到函数被调用,stfld应该使用返回值来设置字段。
然后我查看了汇编代码,看到调用被执行,但是似乎RAX中的返回值被下一次调用覆盖并丢失了。
            Transport = Transport?? MockITransportUtil.GetMock(true);
000007FE9236F776  mov         rax,qword ptr [rbp+0B0h]  
000007FE9236F77D  mov         rax,qword ptr [rax+20h]  
000007FE9236F781  mov         qword ptr [rbp+20h],rax  
000007FE9236F785  mov         rcx,qword ptr [rbp+20h]  
000007FE9236F789  mov         rax,qword ptr [rbp+0B0h]  
000007FE9236F790  mov         qword ptr [rbp+28h],rax  
000007FE9236F794  test        rcx,rcx  
000007FE9236F797  jne         000007FE9236F7AC  
000007FE9236F799  mov         cl,1  
000007FE9236F79B  call        000007FE92290608  

            //var x = ReferenceEquals(null, Transport) ? MockITransportUtil.GetMock(true) : Transport;
            ListerFactory = ListerFactory ?? MockIListenerUtil.GetMockSetupWithAction((a) => invokingAction = a);
000007FE9236F7A0  mov         qword ptr [rbp+30h],rax  
000007FE9236F7A4  mov         rax,qword ptr [rbp+30h]  
000007FE9236F7A8  mov         qword ptr [rbp+20h],rax  
000007FE9236F7AC  mov         rcx,qword ptr [rbp+28h]  

如果我使用if语句或?:运算符,一切都正常工作。
Visual Studio 2013
编辑
我已经创建了一个伪最小化的复制。
class simple
{
    public A MyA = null;
    public B MyB = null;

    public void SetUp()
    {
        MyA = MyA ?? new A();
        MyB = new B();// Put breakpoint here
    }
}

如果您在指定行上设置断点并在调试器中查看MyA的值,它仍将为null(仅在x64构建时)。如果执行下一行,则会设置该值。我无法重现完全没有评估发生的情况。很明显,在分解中,下一行的执行已经开始,在赋值之前。

编辑2

这里是ms connect网站的link链接


这是一个单元测试 x64 调试。 - rerun
@rerun 我应该补充一下,当我尝试创建一个最小的情况来重现这种奇怪的行为时,我无法做到。它只发生在我的大型代码库中。但是它发生的变量是 double? x - 类似于 x = x ?? 1.0;。如果在达到该行时 xnull,那么之后它仍然是 null - Timothy Shields
2
@rerun 那我不是疯了。:) 呼叫Eric Lippert... - Timothy Shields
2
注意,64位调试器存在问题请参见此处 - Guvante
2
@Guvante,这是不同形式的同一个问题,但不是调试器的问题。根据生成的汇编代码,它在错误的位置执行fld赋值操作。看起来可能正在执行不应该执行的优化操作。 - rerun
显示剩余11条评论
2个回答

10
    MyB = new B();// Put breakpoint here

问题在于断点而不是代码生成。x64即时编译器会出现问题,它会生成不准确的调试信息。它会为语句错误地发出行号信息,使用仍然是上一条语句的代码地址。
从您发布的反汇编中可以看出,地址F7A0到F7A8处的代码仍然属于??语句。到F7AC的分支才是真正的下一条语句开始的地方。所以它应该说F7AC是下一条语句的开始,而不是F7A0。
这个 bug 的后果是调试器可能永远不会停在断点上。您可以通过更改重现代码并编写public A MyA = new A();来自己看到这一点。如果它确实停止了,那么赋值还没有执行。因此,在您的情况下,仍然会看到变量具有先前的值,即null。单步解决它,尽管它取决于下一条语句的外观。

请放心,只有在调试时才会出现问题,程序仍然可以正确运行。记住这个小问题,据我所知,这只会影响到 ?? 运算符。你可以看出它并没有被广泛使用 :) 尽管大多数程序员只会调试他们程序的32位版本,但默认的项目设置非常鼓励使用。

问题正在解决中,不要指望您的Connect报告会产生影响,微软已经对此错误有了充分的认识。微软的Jitter团队已经完全重写了x64 Jitter,目前处于CTP2阶段。我估计还需要一年左右才能发布。


这不是问题所在。我之前也遇到过与提问者相同的问题,由此导致程序行为不正确,而不仅仅是调试器问题。此外,行为会因所在机器而异。?? 在一台机器上会失败,在另一台机器上则正常工作,这表明 JIT 中存在错误。 - Timothy Shields
请一次只报告一个错误,请勿报告您自己的错误。 - Hans Passant
我之所以看到这个问题,是因为它作为 null 从发布的函数中传递出来。一旦我对程序进行了任何更改,就会发生赋值。 - rerun
@HansPassant 在 rerun 的链接的 MS Connect bug 报告中,他写道:“在生产代码中,我能够看到赋值从未发生。在最小化的复制中,我无法让它发生。”(强调添加)这正是我遇到的确切问题。在我的生产代码中,空合并运算符完全失效,我不得不切换到替代语法。当我尝试制作一个玩具程序来重现这个问题时,我无法做到。rerun 也遇到了同样的情况。所以看起来我们遇到了同样的 bug。 - Timothy Shields
另外一个关于这个 bug 的方面是,在 Release 构建中打开优化器会发生什么。我可以看到可能性是当优化器认为右侧表达式没有被使用时,它会消除 ?? 运算符的右侧表达式。这很大程度上取决于表达式的样子,例如它不会消除构造函数调用。这很难确定。 - Hans Passant
这很有趣,因为当我将代码从调用构造函数更改为调用执行构造和一些设置的静态函数时,我发现了这个问题。我还在想这是否是JIT没有意识到它处于调试模式,因此不应重新排序代码的问题。另外,我不明白为什么“??”是唯一的问题,因为等效的if语句应该创建几乎相同的IL。 - rerun

1

我收到了来自微软的更新,确认这确实是一个真正的问题,并已在即将发布的x64 jiter版本中修复。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接