Modèle de mémoire: prévention de la réorganisation du magasin et de l’acquisition de charge

Il est connu que, contrairement aux volatiles de Java, ceux de .NET permettent de réordonner les écritures volatiles avec les lectures volatiles suivantes depuis un autre emplacement. En cas de problème, MemoryBarier est recommandé de placer MemoryBarier entre eux ou Interlocked.Exchange peut être utilisé à la place de l’écriture volatile.

Cela fonctionne mais MemoryBarier pourrait être un tueur de performance lorsqu’il est utilisé dans un code sans locking hautement optimisé.

J’y ai un peu réfléchi et suis venu avec une idée. Je veux que quelqu’un me dise si j’ai pris le bon chemin.

Donc, l’idée est la suivante:

Nous voulons empêcher la réorganisation entre ces deux access:

  volatile1 write volatile2 read 

De .NET MM, nous soaps que:

  1) writes to a variable cannot be reordered with a following read from the same variable 2) no volatile accesses can be eliminated 3) no memory accesses can be reordered with a previous volatile read 

Pour éviter toute réorganisation indésirable entre écriture et lecture, introduisons une lecture volatile factice à partir de la variable dans laquelle nous venons d’écrire:

  A) volatile1 write B) volatile1 read [to a visible (accessible | potentially shared) location] C) volatile2 read 

Dans ce cas, B ne peut pas être réorganisé avec A car ils accèdent tous les deux à la même variable, C ne peut pas être réorganisé avec B, car deux lectures volatiles ne peuvent pas être réorganisées ensemble et transitoirement C ne peut pas être réorganisé avec A.

Et la question:

Ai-je raison? Cette lecture volatile factice peut-elle être utilisée comme une barrière de mémoire légère pour un tel scénario?

Ici, je vais utiliser une notation en flèche pour conceptualiser les barrières de mémoire. J’utilise une flèche vers le haut ↑ et une flèche vers le bas pour représenter les écritures et les lectures volatiles, respectivement. Pensez à la pointe de la flèche pour repousser toute autre lecture ou écriture. Ainsi, aucun autre access mémoire ne peut dépasser la tête de la flèche, mais ils peuvent également se déplacer au-delà de la queue.

Considérez votre premier exemple. Voici comment cela serait conceptualisé.

 ↑ volatile1 write // A volatile2 read // B ↓ 

Nous pouvons donc clairement voir que la lecture et l’écriture sont autorisées à changer de position. Vous avez raison.

Considérons maintenant votre deuxième exemple. Vous avez affirmé qu’introduire une lecture factice empêcherait que l’écriture de A et la lecture de B soient échangées.

 ↑ volatile1 write // A volatile1 read // A ↓ volatile2 read // B ↓ 

Nous pouvons voir que B est empêché de remonter par la lecture factice de A Nous pouvons également voir que la lecture de A ne peut pas flotter car, par déduction, ce serait la même chose que B déplaçant avant A Mais notez que nous n’avons aucune flèche ↑ qui empêcherait l’écriture dans A de flotter vers le bas (rappelez-vous qu’elle peut toujours se déplacer au-delà de la queue d’une flèche). Donc non, du moins théoriquement, l’injection d’une lecture factice de A n’empêchera pas l’écriture d’origine de A et la lecture de B d’être échangées, car l’écriture dans A est toujours autorisée à descendre.

Je devais vraiment penser à ce scénario. Une chose à laquelle je réfléchissais depuis un certain temps est de savoir si la lecture et l’écriture vers A sont verrouillées ensemble en tandem. Si tel était le cas, cela empêcherait l’écriture sur A de descendre, car il faudrait qu’elle emporte avec elle la lecture dont nous avions déjà parlé. Donc, si vous vous en tenez à cette école de pensée, votre solution pourrait bien fonctionner. Mais, j’ai relu la spécification et je ne vois rien de particulier mentionné concernant les access volatiles à la même variable. Bien entendu, le thread doit s’exécuter de manière logiquement cohérente avec la séquence de programme originale (mentionnée dans la spécification). Mais, je peux visualiser les moyens que le compilateur ou le matériel pourrait optimiser (ou réorganiser de toute autre manière) cet access en tandem de A tout en obtenant le même résultat. Donc, je dois simplement faire preuve de prudence et supposer que l’écriture dans A peut descendre. Rappelez-vous qu’une lecture volatile ne signifie pas “nouvelle lecture de la mémoire principale”. L’écriture sur A peut être mise en cache dans un registre, puis la lecture provient de ce registre, ce qui retarde l’écriture réelle à une heure ultérieure. Une sémantique volatile n’empêche pas ce scénario pour autant que je sache.

La bonne solution serait de placer un appel à Thread.MemoryBarrier entre les access. Vous pouvez voir comment cela est conceptualisé avec la notation en flèche.

 ↑ volatile1 write // A ↑ Thread.MemoryBarrier ↓ volatile2 read // B ↓ 

Vous pouvez maintenant voir que la lecture n’est pas autorisée à flotter et que l’écriture n’est pas autorisée à flotter pour empêcher le swap.


Vous pouvez voir certaines de mes autres réponses concernant la barrière de mémoire en utilisant cette notation de flèche ici , ici et ici pour n’en nommer que quelques-unes.

J’ai oublié de poster la réponse bientôt trouvée à SO. Mieux vaut tard que jamais..

Il s’avère que c’est impossible grâce à la façon dont les processeurs (au moins du type x86-x64) optimisent les access à la mémoire. J’ai trouvé la réponse quand lisais les manuels d’Intel sur ses procs. Exemple 8-5: “Le transfert intra-processeur est autorisé” semblait suspect. Googler pour “transfert de mémoire tampon” mène aux articles du blog de Joe Duffy ( premier et deuxième – lisez-les svp).

Pour optimiser les écritures, le processeur utilise des mémoires tampons de stockage (files d’attente d’opérateurs d’écriture par processeur). La mise en mémoire tampon des écritures localement lui permet de procéder à l’optimisation suivante: satisfaire les lectures des écritures précédemment mises en mémoire tampon dans le même emplacement mémoire et n’ayant pas encore quitté le processeur. Cette technique s’appelle le transfert magasin-tampon (ou transfert magasin à chargement).

Le résultat final dans notre cas est que, comme la lecture en B est satisfaite à partir d’une mémoire locale (mémoire tampon), elle n’est pas considérée comme une lecture volatile et peut être réorganisée avec des lectures supplémentaires volatiles à partir d’un autre emplacement de mémoire ( C ).

Cela ressemble à une violation de la règle “Les lectures volatiles ne se réorganisent pas les unes avec les autres”. Oui, c’est une violation, mais très rare et exotique. Pourquoi est-ce arrivé? Probablement parce qu’Intel a publié son premier document officiel sur le modèle de mémoire de ses processeurs, des années après que .NET (et son compilateur JIT) aient vu le jour.

La réponse est donc: non, la lecture factice ( B ) n’empêche pas la réorganisation entre A et C et ne peut pas être utilisée comme une barrière de mémoire légère.

EDIT Les conclusions que j’ai tirées des spécifications C # sont fausses, voir ci-dessous. FIN ÉDITER

Je ne suis sûrement pas une personne «autorisée», mais je pense que vous n’avez pas compris le modèle de mémoire correctement.

Citation de la spécification C #, section §10.10 Ordre d’ exécution , troisième puce de la page 105:

L’ordre des effets secondaires est préservé en ce qui concerne les lectures et les écritures volatiles.

Les lectures et écritures volatiles sont définies comme des “effets secondaires” et ce paragraphe indique que l’ordre des effets secondaires est préservé.

Je crois donc que toute votre question repose sur une hypothèse incorrecte: il est impossible de réorganiser les lectures et les écritures volatiles.

Je pense que vous avez été confondu avec ce fait qu’en ce qui concerne les opérations de mémoire non volatile , les lectures et les écritures volatiles ne sont que des demi-barrières.

EDITER cet article: Le modèle de mémoire C # dans Theory and Practice, la partie 2 indique exactement le contraire et confirme votre affirmation selon laquelle les lectures volatiles peuvent aller au-delà d’une écriture volatile sans rapport. La solution suggérée consiste à introduire un MemoryBarrier là où cela compte.

Le commentaire de Daniel ci-dessous indique également que la spécification CLI est plus spécifique à ce sujet que la spécification C # et permet cette réorganisation.

Maintenant, je trouve que la spécification C # que j’ai citée ci-dessus est déroutante! Mais étant donné que sur x86, les mêmes instructions sont utilisées pour un access en mémoire volatile et un access en mémoire normal, il est donc parfaitement logique qu’elles soient soumises aux mêmes problèmes de réorganisation de demi-clôture. FIN ÉDITER

Permettez-moi de ne pas accepter la réponse acceptée par Brian Gideon.

OmariO, votre solution au problème (lecture factice) me convient parfaitement . Comme vous l’avez mentionné correctement, les écritures dans une variable ne peuvent pas être réordonnées avec une lecture suivante de la même variable. Si cette réorganisation était possible, le code serait incorrect dans un cas à un seul thread (l’opération de lecture pourrait ne pas renvoyer la même valeur que celle qui avait été écrite lors de la précédente opération d’écriture). Autrement dit, cela violerait la règle fondamentale de tout modèle de mémoire: l’exécution d’un programme à un seul thread ne doit pas être modifiée logiquement.

Également, Brian et OmariO, ne mélangez pas les opérations de mémoire avec la sémantique d’acquisition / libération et d’acquisition / libération de barrières de mémoire. Par exemple, une opération de lecture-acquisition n’est pas la même chose qu’une clôture d’acquisition. Ils ont une sémantique similaire mais la distinction entre eux est très importante. La meilleure explication de ces termes que je connaisse est dans l’excellent blog de Jeff Preshing:
Acquérir et libérer la sémantique
Acquérir et libérer des clôtures