.NET 3.5 JIT在运行应用程序时无法工作

411
下面的代码在Visual Studio内运行和在Visual Studio外运行时给出不同的输出。我正在使用Visual Studio 2008并针对.NET 3.5进行开发。我还尝试过.NET 3.5 SP1。
在Visual Studio外运行时,JIT应该会启动。要么(a)有一些微妙的C#问题我没有注意到,要么(b)JIT实际上出现了错误。我怀疑JIT可能会出错,但是我已经没有其他选择了...
在Visual Studio内运行时的输出:
    0 0,
    0 1,
    1 0,
    1 1,

在 Visual Studio 之外运行 Release 模式时的输出:

    0 2,
    0 2,
    1 2,
    1 2,

为什么呢?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Test
{
    struct IntVec
    {
        public int x;
        public int y;
    }

    interface IDoSomething
    {
        void Do(IntVec o);
    }

    class DoSomething : IDoSomething
    {
        public void Do(IntVec o)
        {
            Console.WriteLine(o.x.ToString() + " " + o.y.ToString()+",");
        }
    }

    class Program
    {
        static void Test(IDoSomething oDoesSomething)
        {
            IntVec oVec = new IntVec();
            for (oVec.x = 0; oVec.x < 2; oVec.x++)
            {
                for (oVec.y = 0; oVec.y < 2; oVec.y++)
                {
                    oDoesSomething.Do(oVec);
                }
            }
        }

        static void Main(string[] args)
        {
            Test(new DoSomething());
            Console.ReadLine();
        }
    }
}

8
是的,这个问题真是太严重了:在如此关键的.NET JIT中发现一个严重的错误-祝贺! - Andras Zoltan
73
看起来我的12月9日构建的x86架构上的4.0框架也有这个问题。我会把它传给JIT团队。谢谢! - Eric Lippert
28
这是极少数值得获得金徽章的问题之一。 - Mehrdad Afshari
28
我们都对这个问题感兴趣,这表明我们并没有“预料到” .NET JIT 存在漏洞,这是微软做得很好。 - Ian Ringrose
2
我们都急切地等待着微软的回复..... - Talha
3个回答

211

这是一个JIT优化器的bug。它正在展开内部循环,但未正确更新oVec.y值:

      for (oVec.x = 0; oVec.x < 2; oVec.x++) {
0000000a  xor         esi,esi                         ; oVec.x = 0
        for (oVec.y = 0; oVec.y < 2; oVec.y++) {
0000000c  mov         edi,2                           ; oVec.y = 2, WRONG!
          oDoesSomething.Do(oVec);
00000011  push        edi  
00000012  push        esi  
00000013  mov         ecx,ebx 
00000015  call        dword ptr ds:[00170210h]        ; first unrolled call
0000001b  push        edi                             ; WRONG! does not increment oVec.y
0000001c  push        esi  
0000001d  mov         ecx,ebx 
0000001f  call        dword ptr ds:[00170210h]        ; second unrolled call
      for (oVec.x = 0; oVec.x < 2; oVec.x++) {
00000025  inc         esi  
00000026  cmp         esi,2 
00000029  jl          0000000C 

当你让oVec.y增加到4时,这个bug就消失了,这说明调用次数过多造成了问题。

一个解决方法是这样的:

  for (int x = 0; x < 2; x++) {
    for (int y = 0; y < 2; y++) {
      oDoesSomething.Do(new IntVec(x, y));
    }
  }

更新:在2012年8月重新检查后,此错误已在版本4.0.30319的Jitter中修复。 但在v2.0.50727 Jitter中仍然存在。 看起来他们不太可能在这么长时间之后修复旧版本中的问题。


3
+1,明显是一个漏洞 - 我可能已经确定了错误发生的条件(虽然我不是说nobugz是因为我才找到的!),但这个问题(以及你的问题,Nick,所以你也+1)表明JIT是罪魁祸首。有趣的是,当IntVec被声明为类时,优化要么被删除,要么不同。即使在循环之前显式将结构字段初始化为0,仍会出现相同的行为。糟糕! - Andras Zoltan
3
@Hans Passant,您使用了什么工具来输出汇编代码? - user2324540
4
@Joan - 仅使用Visual Studio,从调试器的反汇编窗口复制/粘贴,并手动添加注释。 - Hans Passant

81

我认为这是一个真正的JIT编译错误。我建议向Microsoft报告此问题并查看他们的答复。有趣的是,我发现x64 JIT没有同样的问题。

以下是我对x86 JIT的理解。

// save context
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  push        ebx  

// put oDoesSomething pointer in ebx
00000006  mov         ebx,ecx 

// zero out edi, this will store oVec.y
00000008  xor         edi,edi 

// zero out esi, this will store oVec.x
0000000a  xor         esi,esi 

// NOTE: the inner loop is unrolled here.
// set oVec.y to 2
0000000c  mov         edi,2 

// call oDoesSomething.Do(oVec) -- y is always 2!?!
00000011  push        edi  
00000012  push        esi  
00000013  mov         ecx,ebx 
00000015  call        dword ptr ds:[002F0010h] 

// call oDoesSomething.Do(oVec) -- y is always 2?!?!
0000001b  push        edi  
0000001c  push        esi  
0000001d  mov         ecx,ebx 
0000001f  call        dword ptr ds:[002F0010h] 

// increment oVec.x
00000025  inc         esi  

// loop back to 0000000C if oVec.x < 2
00000026  cmp         esi,2 
00000029  jl          0000000C 

// restore context and return
0000002b  pop         ebx  
0000002c  pop         esi  
0000002d  pop         edi  
0000002e  pop         ebp  
0000002f  ret     

在我看来,这似乎是一种糟糕的优化...


23

我将您的代码复制到一个新的控制台应用程序中。

  • 调试版本
    • 使用调试器和无调试器均可得到正确的输出
  • 切换到发布版本
    • 再次,两次得到了正确的输出
  • 创建了一个新的x86配置(我正在运行X64 Windows 2008并使用“任何CPU”)
  • 调试版本
    • 使用F5和CTRL+F5都得到了正确的输出
  • 发布版本
    • 在附加了调试器时得到了正确的输出
    • 没有调试器 - 得到了错误的输出

所以是x86 JIT不正确地生成了代码。我已经删除了有关重排序循环等的原始文本。这里的一些其他答案已经确认,当在x86上时JIT会不正确地展开循环。

要解决问题,可以将IntVec的声明更改为类,并且它在所有版本中都可以正常工作。

我认为这需要提交到MS Connect....

-1给微软!


1
有趣的想法,但如果是这种情况,这显然不是“优化”,而是编译器中的一个非常严重的错误吧?如果是这样的话,早就应该被发现了,不是吗? - David M
我同意你的看法。重新排列循环可能会导致无数问题。实际上,这似乎更不可能发生,因为for循环永远无法达到2。 - Andras Zoltan
2
看起来像是这些讨厌的海森堡错误之一 :P - arul
如果操作系统(或任何使用他的应用程序)使用的是32位x86机器,那么任何CPU都无法工作。问题在于启用优化的x86 JIT会生成错误的代码。 - Nick Guerrera

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