2012-11-30 36 views
79

我想ping通StackOverflow社區,看看我是否正在用這個簡單的C#代碼丟失我的想法。雙? = double? +雙?

我在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編譯器出了什麼問題,或者出了什麼問題?

+0

ideone運行單聲道,它按預期工作。 http://ideone.com/DbuxwD –

+15

這與我的興趣相關。現在我們只需要等待Skeet先生。 – MikeTheLiar

+3

它可能是調試器?嘗試在最後放置一個Console.WriteLine()並查看它打印出來的內容。 – siride

回答

85

道格拉斯的回答是正確的關於JIT優化死代碼( x86和x64編譯器將這樣做)。但是,如果JIT編譯器優化了死代碼,它將立即顯而易見,因爲x甚至不會出現在本地窗口中。此外,當試圖訪問它時,watch和immediate窗口會給你一個錯誤:「名稱'x'在當前上下文中不存在」。這不是你所描述的情況。

你們看到的實際上是在Visual Studio 2010

首先一個錯誤,我試圖模仿我的主機對這個問題:Win7x64和VS2012。對於.NET 4.0目標,x在關閉大括號中斷時等於3.0D。我決定嘗試.NET 3.5的目標,並且,x也被設置爲3.0D,而不是null。

由於我不能完全再現這個問題,因爲我已經在.NET 4.0之上安裝了.NET 4.5,所以我創建了一個虛擬機並在其上安裝了VS2010。

在這裏,我能夠重現這個問題。在Main方法的關閉花括號上的斷點處,在觀察窗口和當地人窗口中,我看到xnull。這是它開始變得有趣的地方。我將目標定位在v2.0運行時,並發現它也是空的。當然,情況並非如此,因爲我的另一臺成功顯示x的計算機具有相同版本的.NET 2.0運行時,其值爲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()

我不知道這個bug是否曾報告給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定義了在賦值過程中使用的重複局部變量,然後添加額外的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 Thread Id: 0x135c (9) 
Child SP   IP    Call Site 
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main(System.String[]) 
[And so on...] 

如果我們查看了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] 
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,我們從得到!clrstack in Main,我們看到執行被暫停在指令之後,直接在之後調用System.Nullable<double>的構造函數。(int 3是調試器用於停止執行的中斷)我用*標記了該行,並且您還可以在IL中匹配L_0056

下面的x64程序集實際上將它分配給本地變量x。我們的指令指針還沒有執行的代碼還,所以VS2010過早打破之前x變量已經由本機代碼分配。

編輯:在x64中,int 3指令位於賦值代碼之前,如上所示。在x86中,該指令位於分配代碼之後。這就解釋了爲什麼VS只在64位時纔會破產。很難說如果這是Visual Studio或JIT編譯器的錯誤。我不確定哪個應用程序插入斷點鉤子。

+4

+1:貌似合理。謝謝! – Douglas

+6

@ChristopherCurrens好的男人,你贏得了綠色複選標記。這是有道理的。我會向微軟發送一些東西。感謝大家對此的關注。社區的努力給我留下了深刻的印象。 – MrE

30

已知x64 JIT編譯器在其優化方面比x86更具侵略性。 (你可以參考「Array Bounds Check Elimination in the CLR」對於其中的x86和x64編譯器產生的代碼在語義上不同的情況。)

在這種情況下,64位編譯器檢測到x永遠不會被讀取,並且摒棄了它的分配共;這在編譯器優化中被稱爲dead code elimination。爲了防止這種情況發生,只要添加以下行的分配之後:

Console.WriteLine(x); 

你會發現,不僅得到印刷的3正確的值,但是變量x將顯示在調試器中正確的值以及(編輯)Console.WriteLine調用哪個引用它。

編輯:克里斯托弗Currens提供alternative explanation對在Visual Studio 2010中的錯誤,這可能比上述更精確指向。

+0

呃,我剛剛測試過,因爲這也是我的第一個想法,而這個答案實際上是不正確的。在調用Console.WriteLine()之後,調試器實際上在賦值後顯示null。 – tomfanning

+0

@tomfanning:這不會使答案無效。編譯器僅在檢測到需要這樣的點時發出執行分配的指令 - 在「Console.WriteLine」處。 – Douglas

+0

我懷疑編譯器是什麼不同(x86與64位)。這裏可能會看到的是未關閉的JIT優化。 – leppie