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

Ron*_*nin 10 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 );
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

没有 代码优化的情况下运行时,结果如预期._cursorLeft留下尽可能_cursorTop顶部都是平等的.

aa
Left: 2 _ 2
Top: 0 _ 0
bbb
Left: 3 _ 3
Top: 3 _ 3
Run Code Online (Sandbox Code Playgroud)

但是当我使用 代码优化运行它时,两个值_cursorLeft_cursorTop变成了bizzare:

aa
Left: -65534 _ 2
Top: -65536 _ 0
bb
Left: -65533 _ 3
Top: -65533 _ 3
Run Code Online (Sandbox Code Playgroud)

我找到了2个解决方法:

  1. _cursorLeft_cursorTop设置为0而不是-1
  2. 让Interlocked.Exchange取左边的值.最佳

由于解决方法#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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但这种奇怪的行为来自哪里?
有没有更好的解决方法/解决方案?


[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;
}
Run Code Online (Sandbox Code Playgroud)

[由我编辑:]
我想出了另一个更短的例子:

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 );
    }
}
Run Code Online (Sandbox Code Playgroud)

除非您不使用优化或将_intToExchange设置为范围内的值,否则您ushort将无法识别该问题.

Han*_*ant 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
Run Code Online (Sandbox Code Playgroud)

XCHG指令错误,它使用16位操作数(cx和word ptr).但是变量类型需要32位操作数.因此,变量的高16位保持在0xffff,使整个值为负.

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

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