2017-04-04 79 views
10

問題我有一個令人沮喪的問題與一些代碼,不知道爲什麼會出現此問題。C#代碼優化導致Interlocked.Exchange()

// 
// .NET FRAMEWORK v4.6.2 Console App 

static void Main(string[] args) 
{ 
    var list = new List<string>{ "aa", "bbb", "cccccc", "dddddddd", "eeeeeeeeeeeeeeee", "fffff", "gg" }; 

    foreach(var item in list) 
    { 
     Progress(item); 
    } 
} 

private static int _cursorLeft = -1; 
private static int _cursorTop = -1; 
public static void Progress(string value = null) 
{ 
    lock(Console.Out) 
    { 
     if(!string.IsNullOrEmpty(value)) 
     { 
      Console.Write(value); 
      var left = Console.CursorLeft; 
      var top = Console.CursorTop; 
      Interlocked.Exchange(ref _cursorLeft, Console.CursorLeft); 
      Interlocked.Exchange(ref _cursorTop, Console.CursorTop); 
      Console.WriteLine(); 
      Console.WriteLine("Left: {0} _ {1}", _cursorLeft, left); 
      Console.WriteLine("Top: {0} _ {1}", _cursorTop, top); 
     } 
    } 
} 

當沒有那麼結果不出所料代碼優化運行_cursorLeft and left只要_cursorTop頂部是相等的。

aa 
Left: 2 _ 2 
Top: 0 _ 0 
bbb 
Left: 3 _ 3 
Top: 3 _ 3 

但是,當我與代碼優化兩個值_cursorLeft_cursorTop運行成爲的bizzare:

aa 
Left: -65534 _ 2 
Top: -65536 _ 0 
bb 
Left: -65533 _ 3 
Top: -65533 _ 3 

我發現2個解決方法:

  1. 設置_cursorLeft_cursorTop到而不是-1
  2. 讓Interlocked.Exchange取的值從 RESP。 頂部

因爲解決方法1不符合我的需求,我結束了與解決方法2:

private static int _cursorLeft = -1; 
private static int _cursorTop = -1; 
public static void Progress(string value = null) 
{ 
    lock(Console.Out) 
    { 
     if(!string.IsNullOrEmpty(value)) 
     { 
      Console.Write(value); 

      // OLD - does NOT work! 
      //Interlocked.Exchange(ref _cursorLeft, Console.CursorLeft); 
      //Interlocked.Exchange(ref _cursorTop, Console.CursorTop); 

      // NEW - works great! 
      var left = Console.CursorLeft; 
      var top = Console.CursorTop; 
      Interlocked.Exchange(ref _cursorLeft, left); // new 
      Interlocked.Exchange(ref _cursorTop, top); // new 
     } 
    } 
} 

但是哪裏這個古怪的動作從何而來?
是否有更好的解決方法?


[編輯由Matthew沃森:添加簡化REPRO:]

class Program 
{ 
    static void Main() 
    { 
     int actual = -1; 
     Interlocked.Exchange(ref actual, Test.AlwaysReturnsZero); 
     Console.WriteLine("Actual value: {0}, Expected 0", actual); 
    } 
} 

static class Test 
{ 
    static short zero; 
    public static int AlwaysReturnsZero => zero; 
} 

[編輯由我:]
我想出另一個甚至更短的例如:

class Program 
{ 
    private static int _intToExchange = -1; 
    private static short _innerShort = 2; 

    // [MethodImpl(MethodImplOptions.NoOptimization)] 
    static void Main(string[] args) 
    { 
     var oldValue = Interlocked.Exchange(ref _intToExchange, _innerShort); 
     Console.WriteLine("It was: {0}", oldValue); 
     Console.WriteLine("It is: {0}", _intToExchange); 
     Console.WriteLine("Expected: {0}", _innerShort); 
    } 
} 

除非您不使用優化或將_intToExchange設置爲ushort範圍內的值,否則您將無法識別該問題。

+1

我可以重現這一點。 –

+0

我冒昧地添加了一個簡化的repro.You可以合併它或刪除它,只要你認爲合適。 –

+0

@MthetheWWats好主意!我真的認爲它必須是一個特定的問題,但它似乎是一個大錯誤。 – Ronin

回答

7

您正確診斷問題,這是一個優化器錯誤。它特定於64位抖動(又名RyuJIT),它首先在VS2015中開始出貨。您只能通過查看生成的機器碼來查看它。在我的機器上看起來像這樣:

00000135 movsx  rcx,word ptr [rbp-7Ch]  ; Cursor.Left 
0000013a mov   r8,7FF9B92D4754h    ; ref _cursorLeft 
00000144 xchg  cx,word ptr [r8]    ; Interlocked.Exchange 

XCHG指令是錯誤的,它使用16位操作數(cx和word ptr)。但變量類型需要32位操作數。結果,變量的高16位保持在0xffff,使整個值爲負。

表徵這個錯誤有點棘手,不容易隔離。獲取Cursor.Left屬性getter內聯看起來有助於觸發該錯誤,並在其下訪問一個16位字段。顯然,不知何故,讓優化器決定一個16位交換將完成工作。而你的解決方法代碼解決這個問題的原因是,使用32位變量來存儲Cursor.Left/Top屬性會使優化器變成一個好的代碼路徑。

在這種情況下,解決方法非常簡單,除了您找到的解決方法之外,您根本不需要互鎖,因爲lock語句已使代碼成爲線程安全的。請在connect.microsoft.com上報告錯誤,如果您不想花時間,請告知我,我會照顧它。

+1

我會報告它;我已經準備好了文字。 –

+0

@MthetheWWatson好吧 – Ronin

+0

我無法在.net核心中重現這一點,您是否嘗試過這樣做? – Evk

4

我沒有一個確切的解釋,但仍想分享我的發現。這似乎是x64抖動內聯中的一個錯誤,與Interlocked.Exchange結合,這是用本機代碼實現的。這裏是一個簡短的版本來重現,而不使用Console類。

class Program { 
    private static int _intToExchange = -1; 

    static void Main(string[] args) { 
     _innerShort = 2; 
     var left = GetShortAsInt(); 
     var oldLeft = Interlocked.Exchange(ref _intToExchange, GetShortAsInt()); 
     Console.WriteLine("Left: new {0} current {1} old {2}", _intToExchange, left, oldLeft); 
     Console.ReadKey(); 
    } 

    private static short _innerShort; 
    static int GetShortAsInt() => _innerShort; 
} 

所以我們有一個int場和返回int的方法,但真正返回「短」(就像Console.LeftCursor一樣)。如果我們優化和針對x64編譯這個在釋放模式,它會輸出:

new -65534 current 2 old 65535 

什麼情況是抖動的內嵌GetShortAsInt但這樣做在某種程度上不正確。我並不確定爲什麼會發生錯誤。編輯:正如漢斯在他的答案中指出的 - 優化器在這種情況下使用不正確的xchg指令來執行交換。

如果改變這樣的:

[MethodImpl(MethodImplOptions.NoInlining)] 
static int GetShortAsInt() => _innerShort; 

它將按預期工作:

new 2 current 2 old -1 

有了它似乎在第一現場工作的非負值,但確實沒有 - 當_intToExchange超過ushort.MaxValue - 它再次破壞:

private static int _intToExchange = ushort.MaxValue + 2; 
new 65538 current 2 old 1 

因此,考慮到所有這些 - 你的解決方法看起來很好。

+0

也許這是一個好主意,檢查這是否仍然與.net核心發生,然後在github上報告它,因爲這似乎像一個非常奇怪的錯誤。 – Staeff

+0

所以..我們在.net框架中有一個生產錯誤?這是非常嚴重的.. –

+0

你的例子的另一個「解決方法」是:'static int GetShortAsInt()=>轉換。ToInt32(_innerShort);' – Ronin