C# 代码优化导致 Interlocked.Exchange() 出现问题
C# Code optimization causes problems with 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 和 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 个解决方法:
- 将 _cursorLeft 和 _cursorTop 设置为 0 而不是 -1
- 让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
}
}
}
但是这种奇怪的行为从何而来?
还有更好的workaround/solution吗?
[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 );
}
}
除非您不使用 Optimization 或将 _intToExchange 设置为 ushort
范围内的值,否则您不会认识到问题。
我没有确切的解释,但仍然想分享我的发现。这似乎是 x64 抖动内联与 Interlocked.Exchange
相结合的一个错误,它是在本机代码中实现的。这是一个简短的复制版本,没有使用 Console
class.
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
字段和一个 returns int
但实际上 returns '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
鉴于所有这些 - 您的解决方法看起来不错。
您正确诊断了问题,这是一个优化器错误。它特定于 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 报告错误,如果您不想花时间告诉我,我会处理。
我有一段代码遇到了令人沮丧的问题,不知道为什么会出现这个问题。
//
// .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 个解决方法:
- 将 _cursorLeft 和 _cursorTop 设置为 0 而不是 -1
- 让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
}
}
}
但是这种奇怪的行为从何而来?
还有更好的workaround/solution吗?
[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 );
}
}
除非您不使用 Optimization 或将 _intToExchange 设置为 ushort
范围内的值,否则您不会认识到问题。
我没有确切的解释,但仍然想分享我的发现。这似乎是 x64 抖动内联与 Interlocked.Exchange
相结合的一个错误,它是在本机代码中实现的。这是一个简短的复制版本,没有使用 Console
class.
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
字段和一个 returns int
但实际上 returns '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
鉴于所有这些 - 您的解决方法看起来不错。
您正确诊断了问题,这是一个优化器错误。它特定于 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 报告错误,如果您不想花时间告诉我,我会处理。