web-dev-qa-db-fra.com

Optimisations volatiles du compilateur .NET JIT

https://msdn.Microsoft.com/en-us/magazine/jj883956.aspx

Considérons le modèle de boucle d'interrogation:

private bool _flag = true; 
public void Run() 
{
    // Set _flag to false on another thread
    new Thread(() => { _flag = false; }).Start();
    // Poll the _flag field until it is set to false
    while (_flag) ;
    // The loop might never terminate! 
} 

Dans ce cas, le compilateur .NET 4.5 JIT peut réécrire la boucle comme suit:

if (_flag) { while (true); } 

Dans le cas simple thread, cela la transformation est tout à fait légale et, en général, hisser une lecture d'une boucle est une excellente optimisation. Cependant, si le _flag est défini à false sur un autre thread, l'optimisation peut provoquer un blocage.

Notez que si le champ _flag était volatile, le compilateur JIT ne le ferait pas hisser la lecture de la boucle. (Reportez-vous à la section "Boucle de vote" du article De décembre pour une explication plus détaillée de ce modèle.)

Le compilateur JIT optimisera-t-il toujours le code comme indiqué ci-dessus si je verrouille _flag ou le ferai-je seulement volatile pour arrêter l'optimisation?

Eric Lippert a dit ceci à propos de volatile:

Franchement, je vous décourage de jamais créer un domaine instable. Volatil les champs sont un signe que vous faites quelque chose de complètement fou: vous êtes essayer de lire et d’écrire la même valeur sur deux threads différents sans mettre un verrou en place. Les serrures garantissent que la mémoire lue ou les modifications apportées à l’intérieur de la serrure sont compatibles, la garantie des serrures qu'un seul thread accède à un bloc de mémoire donné à la fois, et bientôt. Le nombre de situations dans lesquelles un verrou est trop lent est très petit, et la probabilité que vous vous trompiez de code parce que vous ne comprenez pas le modèle de mémoire exact est très grand. JE n'essayez pas d'écrire un code de bas niveau de verrouillage, à l'exception du plus trivial usages des opérations interverrouillées. Je laisse l'usage de "volatile" à de vrais experts.

Pour résumer: Qui s'assure que l'optimisation mentionnée ci-dessus ne détruit pas mon code? Seulement volatile? Aussi la déclaration lock? Ou autre chose?

Comme Eric Lippert vous dissuade d’utiliser volatile, il doit y avoir autre chose?


Downvoters: J'apprécie chaque réponse à la question. Surtout si vous avez un vote négatif, j'aimerais savoir pourquoi vous pensez que c'est une mauvaise question.


Une variable bool n'est pas une primitive de synchronisation de thread: La question est conçue comme une question générique. Quand le compilateur ne fera-t-il pas l'optimisation?


Dupilcate: Cette question concerne explicitement les optimisations. Celui que vous avez lié ne mentionne pas les optimisations.

13
NtFreX

Répondons à la question qui a été posée:

Le compilateur JIT optimisera-t-il toujours le code comme indiqué ci-dessus si je verrouille _flag ou ne le rendra que volatil pour arrêter l'optimisation?

OK, ne répondons pas à la question qui a été posée, car cette question est trop compliquée. Divisons cela en une série de questions moins compliquées.

Le compilateur JIT optimisera-t-il toujours le code comme indiqué ci-dessus si je verrouille _flag?

Réponse courte: lock donne une garantie plus forte que volatile, donc non, la gigue ne sera pas autorisée à extraire la lecture de la boucle s'il existe un verrou autour de la lecture de _flag. Bien sûr le verrou doit également être autour de l'écriture . Les serrures ne fonctionnent que si vous les utilisez partout.

private bool _flag = true; 
private object _flagLock = new object();
public void Run() 
{
  new Thread(() => { lock(_flaglock) _flag = false; }).Start();
  while (true)
    lock (_flaglock)
      if (!_flag)
        break;
} 

(Et bien sûr, je remarque que c’est un incroyablement mauvais moyen d’attendre qu’un thread en signale un autre. Ne restez jamais dans une boucle serrée en scrutant un drapeau! Utilisez une poignée d’attente comme une personne sensible.)

Vous avez dit que les verrous étaient plus forts que les volatiles; Qu'est-ce que ça veut dire?

Les lectures sur les volatiles empêchent certaines opérations d'être déplacées dans le temps. Les écritures sur les volatiles empêchent certaines opérations d'être déplacées dans le temps. Les verrous empêchent les opérations more d'être déplacées dans le temps. Cette sémantique de prévention est appelée "barrière de mémoire". En gros, les volatiles introduisent une demi-barrière, les serrures une barrière complète.

Lisez la section de spécification C # sur les effets secondaires spéciaux pour plus de détails.

Comme toujours, je vous rappellerai que les volatiles ne vous garantissent pas une fraîcheur globale. La programmation C # multithread n’existe pas en tant que "dernière" valeur d’une variable et les lectures volatiles ne vous donnent donc pas la "dernière" valeur car elle n’existe pas. L'idée qu'il existe une "dernière" valeur implique que les lectures et les écritures ont toujours un ordre cohérent dans le temps, ce qui est faux. Les threads peuvent toujours être en désaccord sur l'ordre des lectures et écritures volatiles.

Les serrures empêchent cette optimisation; les volatiles empêchent cette optimisation. Sont-ce la seule chose qui empêche l'optimisation?

Non. Vous pouvez également utiliser les opérations Interlocked ou introduire explicitement des barrières de mémoire.

Est-ce que je comprends suffisamment pour utiliser volatile correctement?

Nan.

Que devrais-je faire?

N'écrivez pas de programmes multithread en premier lieu. Plusieurs threads de contrôle dans un programme sont une mauvaise idée.

Si vous devez, ne partagez pas de mémoire sur des threads. Utilisez les threads en tant que processus peu coûteux et ne les utilisez que lorsque vous avez un processeur inactif pouvant effectuer une tâche gourmande en ressources. Utilisez une asynchronie à un seul thread pour toutes les opérations d'E/S.

Si vous devez partager de la mémoire sur plusieurs threads, utilisez la construction de programmation du plus haut niveau disponible, pas le le plus bas. Utilisez un CancellationToken pour représenter une opération annulée ailleurs dans un flux de travail asynchrone. 

11
Eric Lippert

Lorsque vous utilisez une lock ou Interlocked Operations , vous dites au compilateur que les blocs ou les emplacements de mémoire ont des courses de données et qu'il ne peut pas présumer de l'accès à ces emplacements. Ainsi, le compilateur recule sur les optimisations qu'il pourrait autrement effectuer dans un environnement sans course de données. Ce contrat implicite signifie également que vous indiquez au compilateur que vous allez accéder à ces emplacements de manière appropriée, sans aucune course aux données. 

2
Mgetz

Le mot clé C # volatile implémente la sémantique dite d'acquisition et de publication. Il est donc tout à fait légitime de l'utiliser pour effectuer une synchronisation de threads simple. Et tout moteur JIT conforme aux normes ne doit pas l’optimiser à l’extérieur.

Bien que, bien sûr, il s’agisse d’une caractéristique du langage parasite, car le C/C++ a une sémantique différente, et c’est celle que la plupart des programmeurs ont peut-être déjà connue. Ainsi, l'utilisation de "volatile" spécifique à C # et à Windows (à l'exception de l'architecture ARM) est parfois source de confusion.

0
John Z. Li