1. ホーム
  2. c#

double? = double? + double?

2023-08-05 11:57:28

質問

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

私は Windows 7 で開発しており、.NET 4.0, x64 Debug でこれを構築しています。

私は以下のコードを持っています。

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

    double? x;
    x = y + z;
}

デバッグして最後の中括弧にブレークポイントを置くと、ウォッチウィンドウとイミディエイトウィンドウにx = 3が表示されるはずですが、代わりにx = nullが表示されます。

x86 でデバッグすると、正常に動作するようです。 x64 コンパイラーに何か問題があるのでしょうか、それとも私に何か問題があるのでしょうか?

どのように解決するのですか?

Douglas の回答は、JIT がデッドコードを最適化することについては正しいです ( 両方とも x86 と x64 のコンパイラーはこれを行います)。 しかし、もし JIT コンパイラがデッド コードを最適化しているとしたら、それはすぐにわかることです。 x はLocalsウィンドウに表示されないからです。 さらに、ウォッチ&イミディエイトウィンドウでは、それにアクセスしようとすると、代わりに次のようなエラーが表示されます: "The name 'x' does not exist in the current context"(名前 'x' は現在のコンテキストには存在しません)。 これは、あなたが説明したようなことが起こっているのではありません。

あなたが見ているものは、実際には Visual Studio 2010 のバグです。

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

.NET 4.0 の上に .NET 4.5 をインストールしているので、この問題の完全な再現はできないので、仮想マシンをスピンアップして、その上に VS2010 をインストールしました。

ここで、問題を再現することができました。 ブレークポイントを Main メソッドの閉じる中括弧にブレークポイントを設定すると、ウォッチ ウィンドウとローカル ウィンドウの両方で、次のようになりました。 xnull . ここからが面白いところです。 代わりにv2.0ランタイムをターゲットにしたところ、そこでもnullであることがわかりました。 私の別のコンピューターには、同じバージョンの .NET 2.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() .

このバグが Microsoft Connect に報告されたことがあるかどうかは知りませんが、このコードを例にして報告したほうがいいかもしれません。 しかし、これは明らかに 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 は代入中に使用するローカル変数を重複して定義し、CS* 変数からそれぞれのユーザー定義ローカル変数にコピーするために余分な IL コマンドを追加しています。 以下は、この現象を示す対応する 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)
子SPのIPコールサイト
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main(System.String[])
[といった具合に...〕。]

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

000007ff`00173388 e813fe25f2 call 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] (RSP+80h)。
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

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

続く x64 アセンブリでは、実際にローカル変数に代入される x . 命令ポインターはまだそのコードを実行していないため、VS2010 は x 変数がネイティブコードによって代入される前に、VS2010 は早期にブレークします。

EDIT: x64 では int 3 命令は、上で見たように、代入コードの前に置かれます。 x86では、その命令は代入コードの後に配置されます。 そのため、x64でだけVSが早く壊れるのはこのためです。 これがVisual Studioのせいなのか、JITコンパイラのせいなのか、判断に迷うところです。 どちらのアプリケーションがブレークポイントフックを挿入しているかは不明です。