79

StackOverflowコミュニティにpingを送信して、この単純なC#コードで頭がおかしくなっているかどうかを確認したいと思いました。

私はWindows7で開発しており、これを.NET 4.0、x64デバッグで構築しています。

私は次のコードを持っています:

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

    double? x;
    x = y + z;
}

デバッグして、最後の中括弧にブレークポイントを設定すると、ウォッチウィンドウとイミディエイトウィンドウでx=3になると予想されます。代わりにx=null。

x86でデバッグすると、問題なく動作するようです。x64コンパイラに問題がありますか、それとも私に問題がありますか?

4

2 に答える 2

86

ダグラスの答えは、JIT最適化デッドコードについて正しいです(x86コンパイラとx64コンパイラの両方がこれを行います)。ただし、JITコンパイラがデッドコードを最適化している場合はx、[ローカル]ウィンドウにも表示されないため、すぐにわかります。さらに、ウォッチとイミディエイトウィンドウにアクセスしようとすると、代わりに「名前'x'は現在のコンテキストに存在しません」というエラーが表示されます。それはあなたが起こっているとあなたが説明したことではありません。

表示されているのは、実際にはVisualStudio2010のバグです。

まず、メインマシンであるWin7x64とVS2012でこの問題を再現しようとしました。.NET 4.0ターゲットのx場合、閉じ中括弧で壊れたときは3.0Dに等しくなります。.NET 3.5ターゲットも試してみることにしましたx。これにより、nullではなく3.0Dに設定されました。

.NET4.0の上に.NET4.5をインストールしているため、この問題を完全に再現することはできないため、仮想マシンを起動してVS2010をインストールしました。

ここで、問題を再現することができました。メソッドの閉じ中括弧にブレークポイントがありMain、ウォッチウィンドウとローカルウィンドウの両方で、それがであることがわかりましxnull。これはそれが面白くなり始めるところです。代わりにv2.0ランタイムをターゲットにしたところ、そこでもnullであることがわかりました。他のコンピューターに同じバージョンの.NET2.0ランタイムがxあり、値が.で正常に表示されているため、これは当てはまりません3.0D

では、何が起こっているのでしょうか。windbgを掘り下げた後、問題が見つかりました。

VS2010は、実際に割り当てられる前の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()

このバグがMicrosoftConnectに報告されたことがあるかどうかはわかりませんが、このコードを例として、報告することをお勧めします。ただし、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
OSスレッドID:0x135c(9)
子SPIPコールサイト
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main(System.String [])
[等々...]

メソッドのネイティブマシンコードを表示するMainと、VSが実行を中断したときに実行された命令を確認できます。

000007ff`00173388e813fe25f2呼び出し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 add rsp、0D8h
000007ff`001733d6 c3 ret

!clrstackで取得した現在のIPを使用すると、のコンストラクターを呼び出した直後Mainに、命令の実行が中断されたことがわかります。(デバッガーが実行を停止するために使用する割り込みです)その行を*で囲みました。また、行をILで一致させることもできます。System.Nullable<double>int 3L_0056

次のx64アセンブリは、実際にそれをローカル変数に割り当てますx。命令ポインターはまだそのコードを実行していないため、VS2010は、x変数がネイティブコードによって割り当てられる前に、時期尚早に壊れています。

編集:x64では、上記のように、int 3命令は割り当てコードの前に配置されます。x86では、その命令は割り当てコードの後に​​配置されます。これが、VSがx64でのみ早期に機能しなくなっている理由を説明しています。これがVisualStudioまたはJITコンパイラのせいであるかどうかを判断するのは難しいです。どのアプリケーションがブレークポイントフックを挿入するかわかりません。

于 2012-11-30T18:38:43.693 に答える
30

x64 JITコンパイラは、x86よりも最適化に積極的であることが知られています。(「<ahref ="http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspx"rel」を参照できます。 x86コンパイラとx64コンパイラが意味的に異なるコードを生成する場合は、「CLRでの配列境界チェックの省略」を参照してください。)

この場合、x64コンパイラは、x読み取りが行われないことを検出し、その割り当てを完全に削除します。これは、コンパイラ最適化におけるデッドコード除去として知られています。これを防ぐには、割り当ての直後に次の行を追加します。

Console.WriteLine(x);

3getの正しい値が出力されるだけでなく、変数がそれを参照する呼び出しの後xにデバッガーでも正しい値を表示(編集)することがわかります。Console.WriteLine

編集:Christopher Currensは、Visual Studio 2010のバグを指摘する別の説明を提供しています。これは、上記よりも正確である可能性があります。

于 2012-11-30T15:45:58.690 に答える