C#代码优化导致Interlocked.Exchange()出现问题

时间:2017-04-04 12:06:05

标签: c# optimization release interlocked

我对一些代码感到沮丧,并且不知道为什么会出现这个问题。

//
// .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 left ,只要 _cursorTop top 相等。

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

但是当我运行 代码优化时, _cursorLeft _cursorTop 这两个值变成了古怪:

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

我找到了2个解决方法:

  1. _cursorLeft _cursorTop 设置为 0 而不是-1
  2. 让Interlocked.Exchange从 left resp获取值。
  3. 由于解决方法#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 Watson编辑:添加简化的复制品:]

    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范围内的值,否则您将无法识别问题。

2 个答案:

答案 0 :(得分: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,使整个值为负。

表征这个bug有点棘手,不容易隔离。获取内联的Cursor.Left属性getter似乎有助于触发错误,在它访问16位字段的引擎盖下。显然,以某种方式使优化器决定16位交换将完成工作。您的变通方法代码解决它的原因是,使用32位变量来存储Cursor.Left / Top属性会使优化器陷入良好的代码路径。

这种情况下的解决方法非常简单,除了您找到的解决方法之外,您根本不需要Interlocked,因为lock语句已经使代码线程安全。请在connect.microsoft.com上报告错误,如果您不想花时间让我知道,我会处理它。

答案 1 :(得分:4)

我没有确切的解释,但仍想分享我的发现。它似乎是与本地代码中实现的Interlocked.Exchange结合使用的x64抖动内联中的错误。这是一个重现的简短版本,不使用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的方法,但确实返回了&#39;短&#39; (就像Console.LeftCursor一样)。如果我们在发布模式下使用优化AND和x64编译它,它将输出:

new -65534 current 2 old 65535

抖动内联GetShortAsInt会发生什么,但这样做不正确。我不确定为什么出了问题。编辑:正如Hans在他的回答中指出的那样 - 优化器在这种情况下使用不正确的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

所以这一切 - 你的解决方法看起来很好。