次のコードは、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();
}
}
}
これは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ジッターにはまだ存在しています。この長い期間を経て古いバージョンでこれを修正する可能性は低いようです。
これは本物の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
これは最適化がうまくいかないように見えます...
コードを新しいコンソールアプリにコピーしました。
そのため、x86 JITが誤ってコードを生成しています。ループの並べ替えなどについての元のテキストを削除しました。ここに関する他のいくつかの回答は、x86でJITがループを誤って巻き戻していることを確認しています。
この問題を修正するには、IntVecの宣言をクラスに変更すると、すべてのフレーバーで機能します。
これはMS Connectに行く必要があると思います...
-1 to Microsoft!