【问题标题】:C# Code optimization causes problems with Interlocked.Exchange()C# 代码优化导致 Interlocked.Exchange() 出现问题
【发布时间】:2017-08-29 15:02:40
【问题描述】:

我对一些代码有一个令人沮丧的问题,不知道为什么会出现这个问题。

//
// .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 );
        }
    }
}

不运行 代码优化时,结果如预期。 _cursorLeftleft_cursorToptop 相等。

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

但是当我运行它时 with 代码优化 两个值 _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 分别取值。 顶部

因为解决方法 #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 范围内的值,否则您不会发现问题。

【问题讨论】:

  • 我可以重现这个。
  • 我冒昧地添加了一个简化的复制品。您可以根据需要合并或删除它。
  • @MatthewWatson 好主意!我真的认为它必须是一个特定的问题,但它似乎是一个大错误。
  • 以后应该会修复:github.com/dotnet/coreclr/issues/10714

标签: c# optimization release interlocked


【解决方案1】:

我没有确切的解释,但仍然想分享我的发现。这似乎是 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 但实际上返回'short' 的方法(就像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

因此,鉴于所有这些 - 您的解决方法看起来不错。

【讨论】:

  • 也许最好检查一下 .net 核心是否仍然发生这种情况,然后在 github 上报告它,因为这似乎是一个非常奇怪的错误。
  • 您的示例的另一个“解决方法”是:static int GetShortAsInt() =&gt; Convert.ToInt32(_innerShort);
  • 好的,另一种解决方法:删除 _innerShort = 2; 并设置 private static short _innerShort = 2;
  • @Ronin 我专门制作了这个例子,让它不能正常工作:)
  • @Evk 我绝对知道。我只是想报告这种奇怪的行为。
【解决方案2】:

您正确诊断了问题,这是一个优化器错误。它特定于 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 和字 ptr)。但变量类型需要 32 位操作数。结果,变量的高 16 位保持在 0xffff,使整个值变为负数。

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

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

【讨论】:

  • @MatthewWatson 好的
  • 我无法在 .net core 中重现此内容,您是否尝试过这样做?
  • 我没有。不太可能重现,使 Console 类跨平台,因此它可以在 Linux 和 OSX 上运行肯定会阻止该属性易于内联。
  • 但是您可以使用返回类型为 int 但内部返回短的方法来重现此问题,根本无法访问 Console 类。
猜你喜欢
  • 2014-04-12
  • 1970-01-01
  • 1970-01-01
  • 2013-01-03
  • 1970-01-01
  • 2012-03-16
  • 1970-01-01
  • 2014-12-30
  • 1970-01-01
相关资源
最近更新 更多