2017-04-04 1 views
10

J'ai un problème frustrant avec un peu de code et je ne sais pas pourquoi ce problème se produit.L'optimisation du code C# provoque des problèmes avec 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); 
     } 
    } 
} 

Lors de l'exécution sansOptimisation du code alors le résultat est comme prévu. _cursorLeft et gauche aussi loin que _cursorTop et top sont égaux.

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

Mais quand je le lance avecOptimisation du code les valeurs _cursorLeft et _cursorTop deviennent bizzare:

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

J'ai trouvé 2 solutions de contournement:

  1. définir _cursorLeft et _cursorTop à au lieu de -1
  2. laisser Interlocked.Exchange prendre la valeur de gauche resp. top

Parce que solution de contournement # 1 ne correspond pas à mes besoins j'ai fini avec solution de contournement # 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 
     } 
    } 
} 

Mais d'où vient ce comportement bizarre vient?
Et y a-t-il une meilleure solution de contournement/solution?


[Modifier par Matthew Watson: Ajout repro simplifiée:]

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

[Modifier par moi:]
je me suis dit un autre exemple encore plus court:

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

À moins que vous n'utilisiez pas Optimisation ou définir _intToExchange à une valeur dans la plage de ushort vous ne reconnaîtriez pas le problème.

+1

Je peux reproduire ceci. –

+0

Je me suis permis d'ajouter une repro simplifiée. Vous pouvez l'incorporer ou l'effacer comme bon vous semble. –

+0

@MatthewWatson Bonne idée! Je pensais vraiment que cela devait être un problème spécifique, mais cela semble être un gros problème. – Ronin

Répondre

7

Vous avez correctement diagnostiqué le problème, c'est un bug de l'optimiseur. Il est spécifique à la gigue 64 bits (aka RyuJIT), celle qui a commencé à être livrée dans VS2015. Vous ne pouvez le voir qu'en regardant le code machine généré. On dirait que cela sur ma machine:

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

L'instruction XCHG est mauvaise, il utilise 16 bits opérandes (cx et mot PTR). Mais le type de variable nécessite des opérandes de 32 bits. Par conséquent, les 16 bits supérieurs de la variable restent à 0xffff, rendant toute la valeur négative.

Caractériser ce bug est un peu délicat, il n'est pas facile à isoler. Obtenir la propriété Cursor.Left getter inlined semble être déterminant pour déclencher le bug, sous le capot, il accède à un champ de 16 bits. Apparemment assez pour, d'une certaine manière, faire l'optimiseur décider qu'un échange de 16 bits fera le travail. Et la raison pour laquelle votre code de contournement l'a résolu, en utilisant des variables 32 bits pour stocker les propriétés Cursor.Left/Top heurte l'optimiseur dans un bon chemin de code.

La solution de contournement dans ce cas est assez simple, au-delà de celle que vous avez trouvée, vous n'avez pas besoin du tout d'interverrouillage car l'instruction lock rend déjà le code thread-safe. S'il vous plaît signaler le bug à connect.microsoft.com, laissez-moi savoir si vous ne voulez pas prendre le temps et je vais m'en occuper.

+1

Je vais le signaler; J'ai déjà préparé le texte. –

+0

@MatthewWatson Okay – Ronin

+0

Je n'étais pas capable de reproduire cela dans le noyau. Net, avez-vous essayé de le faire? – Evk

4

Je n'ai pas d'explication exacte, mais j'aimerais quand même partager mes découvertes. Il semble être un bug dans la jitter x64 en combinaison avec Interlocked.Exchange qui est implémenté en code natif. Voici une version courte à reproduire, sans utiliser la classe 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; 
} 

Nous avons donc un champ int et une méthode qui retourne int mais vraiment retourne « court » (tout comme Console.LeftCursor fait). Si nous compilons ce en mode release avec des optimisations et pour x64, il affichera:

new -65534 current 2 old 65535 

Qu'est-ce qui se passe est inline gigue GetShortAsInt mais cela en quelque sorte de manière incorrecte. Je ne suis pas vraiment sûr de savoir pourquoi exactement les choses vont mal. EDIT: comme le souligne Hans dans sa réponse - l'optimiseur utilise incorrect xchg instuction dans ce cas pour effectuer comme échange.

Si vous changez comme ceci:

[MethodImpl(MethodImplOptions.NoInlining)] 
static int GetShortAsInt() => _innerShort; 

Il fonctionnera comme prévu:

new 2 current 2 old -1 

Avec des valeurs non-négatives, il semble fonctionner au premier site, mais ne pas vraiment - quand _intToExchange dépasse ushort.MaxValue - il se brise à nouveau:

private static int _intToExchange = ushort.MaxValue + 2; 
new 65538 current 2 old 1 

Donc, compte tenu de tout cela - votre solution de contournement semble bien.

+0

Peut-être que ce serait une bonne idée de vérifier si cela se produit encore avec .net core, puis de le signaler sur github, parce que cela semble comme un bug vraiment bizarre. – Staeff

+0

Alors ... nous avons un bug de production dans le framework .net? c'est très sérieux .. –

+0

Une autre "solution de contournement" pour votre exemple est: 'static int GetShortAsInt() => Convertir.ToInt32 (_innerShort); ' – Ronin