Search code examples
c#optimizationreleaseinterlocked

C# Code optimization causes problems with Interlocked.Exchange()


I have a frustrating problem with a bit of code and don't know why this problem occurs.

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

When running without Code optimization then the result is as expected. _cursorLeft and left as far as _cursorTop and top are equal.

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

But when I run it with Code optimization both values _cursorLeft and _cursorTop become bizzare:

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

I found out 2 workarounds:

  1. set _cursorLeft and _cursorTop to 0 instead of -1
  2. let Interlocked.Exchange take the value from left resp. top

Because workaround #1 does not match my needs I ended up with workaround #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
        }
    }
}

But where does this bizarre behavior comes from?
And is there a better workaround/solution?


[Edit by Matthew Watson: Adding simplified 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;
}

[Edit by me:]
I figured out another even shorter example:

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

Unless you don't use Optimization or set _intToExchange to a value in the range of ushort you would not recognize the problem.


Solution

  • You diagnosed the problem correctly, this is an optimizer bug. It is specific to the 64-bit jitter (aka RyuJIT), the one that first started shipping in VS2015. You can only see it by looking at the generated machine code. Looks like this on my machine:

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

    The XCHG instruction is wrong, it uses 16-bit operands (cx and word ptr). But the variable type requires 32-bit operands. As a consequence, the upper 16-bits of the variable remain at 0xffff, making the entire value negative.

    Characterizing this bug is a bit tricky, it is not easy to isolate. Getting the Cursor.Left property getter inlined appears to be instrumental to trigger the bug, under the hood it accesses a 16-bit field. Apparently enough to, somehow, make the optimizer decide that a 16-bit exchange will get the job done. And the reason why your workaround code solved it, using 32-bit variables to store the Cursor.Left/Top properties bumps the optimizer into a good codepath.

    The workaround in this case is a pretty simple one, beyond the one you found, you don't need Interlocked at all because the lock statement already makes the code thread-safe. Please report the bug at connect.microsoft.com, let me know if you don't want to take the time and I'll take care of it.