.NET JIT潜在的なエラー?


404

次のコードは、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の外部でリリースを実行したときの出力:

    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
これは、x86上の4.0フレームワークの12月9日のビルドで再現されるようです。それをジッタチームに渡します。ありがとう!
Eric Lippert、2013年

28
これは、実際にゴールドバッジに値する非常に数少ない質問の1つです。
Mehrdad Afshari、2010年

28
私たち全員がこの質問に関心を持っているという事実は、.NET JIT、よくできたMicrosoftのバグを予期していないことを示しています。
Ian Ringrose 2010

2
私たちは皆、心配そうにMicrosoftの返信を待っています.....
Talha

回答:


211

これはJITオプティマイザのバグです。内部ループを展開していますが、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に増加させると、バグは消えます。これは、アンロールする呼び出しが多すぎます。

1つの回避策はこれです。

  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ジッターで修正されました。しかし、まだv2.0.50727ジッタに存在しています。彼らがこの長い間、古いバージョンでこれを修正する可能性は低いようです。


3
+1、間違いなくバグ-私はエラーの条件を特定した可能性があります(しかし、nobugzが私のためにそれを見つけたとは言わないでください!)、しかしこれ(そしてあなたのニック、あなたも+1)はJITを示しています犯人です。興味深いことに、IntVecがクラスとして宣言されている場合、最適化は削除されるか、または異なります。ループの前に構造体フィールドを最初に明示的に0に初期化しても、同じ動作が見られます。不快な!
Andras Zoltan

3
@Hans Passantアセンブリコードを出力するためにどのツールを使用しましたか?

3
@Joan-Visual Studioのみで、デバッガーの逆アセンブリウィンドウからコピー/貼り付けし、コメントを手動で追加します。
ハンスパッサント2013年

82

これは本物のJITコンパイルのバグだと思います。私はそれをマイクロソフトに報告し、彼らの発言を確認します。興味深いことに、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からMicrosoft!


1
興味深いアイデアですが、確かにこれは「最適化」ではありませんが、これが当てはまる場合、コンパイラーの非常に大きなバグでしょうか。今までに発見されたのではないでしょうか?
David M

仰るとおりです。このようにループを並べ替えると、予期しない問題が発生する可能性があります。ループのためのこれまでの2に達することができないので、実際にこれは、さえにくいようだ
アンドラーシュゾルタン

2
これらの厄介なハイゼンバグの1つであるように見えます:P
arul

OP(または彼のアプリケーションを使用している人)が32ビットx86マシンを持っている場合、CPUは機能しません。問題は、最適化を有効にしたx86 JITが不正なコードを生成することです。
Nick Guerrera、2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.