Quel est le but de L'instruction "PAUSE" dans x86?

j'essaie de créer une version muette d'une serrure de tour. En parcourant le web, Je suis tombé sur une instruction d'assemblage appelée "PAUSE" dans x86 qui est utilisée pour indiquer à un processeur qu'un spin-lock est en cours d'exécution sur ce CPU. Le manuel intel et d'autres informations disponibles indiquent que

le processeur utilise cette indication pour éviter la violation de l'ordre de mémoire dans la plupart des situations, ce qui améliore considérablement les performances du processeur. Pour cette raison, il il est recommandé d'insérer une instruction de PAUSE tous les spin-attendre boucles. La documentation mentionne également que "l'attente(certains delay) " est la pseudo mise en œuvre de l'instruction.

la dernière ligne du paragraphe ci-dessus est intuitive. Si Je ne réussis pas à saisir la serrure, je dois attendre un certain temps avant de saisir à nouveau la serrure.

cependant, qu'entendons-nous par violation de l'ordre de mémoire en cas de verrouillage de spin? "La mémoire de l'ordre violation " signifie la mauvaise spéculative charge/emmagasin des instructions après spin-lock?

la question de spin-lock a déjà été posée sur le débordement de la pile, mais la question de violation de l'ordre de mémoire reste sans réponse (au moins pour ma compréhension).

44
demandé sur Flow 2012-10-15 14:52:44

2 réponses

imaginez comment le processeur exécuterait une boucle spin-wait typique:

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    JMP Spin_Lock
5 Get_Lock:

après quelques itérations, le prédicteur de branche prédira que la branche conditionnelle (3) ne sera jamais prise et que le pipeline se remplira avec les instructions CMP (2). Cela continue jusqu'à ce qu'un autre processeur écrive un zéro à lockvar. À ce stade, nous avons le pipeline plein d'instructions spéculatives (c.-à-d. pas encore engagées) CMP dont certains ont déjà lu lockvar et a rapporté un résultat (incorrect) non nul à la branche conditionnelle suivante (3) (également spéculatif). C'est le moment où la violation de l'ordre de mémoire se produit. Chaque fois que le processeur "voit" une écriture externe (une écriture à partir d'un autre processeur), il cherche dans son pipeline des instructions qui, de façon spéculative, ont accédé au même emplacement de mémoire et ne se sont pas encore propagées. Si de telles instructions sont trouvées, alors l'état spéculatif du processeur est invalide et est effacé avec un rinçage de pipeline.

malheureusement ce scénario va (très probablement) répéter chaque fois qu'un processeur attend sur un spin-lock et rendre ces serrures beaucoup plus lentes qu'elles ne devraient l'être.

entrez l'instruction PAUSE:

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    PAUSE            ; Wait for memory pipeline to become empty
5    JMP Spin_Lock
6 Get_Lock:

l'instruction de PAUSE va" dé-pipeline " la mémoire lit, de sorte que le pipeline n'est pas rempli d'instructions CMP (2) spéculatives comme dans le premier exemple. (C'est-à-dire: il pourrait bloquer le pipeline jusqu'à ce que toutes les anciennes instructions de mémoire sont engagés.) Parce que les instructions CMP (2) s'exécutent séquentiellement, il est peu probable (i.e. la fenêtre de temps est beaucoup plus courte) qu'une écriture externe se produise après l'instruction CMP (2) read lockvar mais avant que le CMP ne soit engagé.

bien sûr, "de-pipelining" gaspillera aussi moins d'énergie dans le spin-lock et dans le cas de l'hyperthreading, il ne gaspillera pas les ressources que l'autre fil pourrait utiliser mieux. D'autre part, il y a encore une branche de prévision erronée en attente de se produire avant chaque sortie de boucle. La documentation d'Intel ne suggère pas que la PAUSE élimine la flush du pipeline, mais qui sait...

67
répondu Mackie Messer 2012-10-16 10:06:49

comme le dit @Mackie, le pipeline se remplira de cmp S. Intel devra purger ces cmp s quand un autre noyau écrit, ce qui est une opération coûteuse. Si le CPU ne le tire pas à la chasse, alors vous avez une violation de l'ordre de mémoire. Un exemple d'une telle violation serait le suivant:

(commençant par lock1 = lock2 = lock3 = var = 1)

Thread 1:

spin:
cmp lock1, 0
jne spin
cmp lock3, 0 # lock3 should be zero, Thread 2 already ran.
je end # Thus I take this path
mov var, 0 # And this is never run
end:

Filetage 2:

mov lock3, 0
mov lock1, 0
mov ebx, var # I should know that var is 1 here.

tout d'abord, considérer Fil 1:

si la branche cmp lock1, 0; jne spin prédit que le lock1 n'est pas zéro, elle ajoute cmp lock3, 0 au pipeline.

Dans le pipeline, cmp lock3, 0 , lit-lock3, et découvre qu'il soit égal à 1.

maintenant, supposons que le fil 1 prend son temps doux, et le fil 2 commence à courir rapidement:

lock3 = 0
lock1 = 0

maintenant, revenons au fil 1:

disons que le cmp lock1, 0 lit finalement lock1, découvre que lock1 est 0, et est heureux de sa capacité prédictive de branche.

cette commande se propage, et rien n'est rincé. Une prédiction correcte de la branche signifie que rien n'est effacé, même avec des lectures hors-ordre, puisque le processeur a déduit qu'il n'y a pas de dépendance interne. lock3 ne dépend pas de lock1 aux yeux du CPU, donc tout va bien.

Maintenant, le cmp lock3, 0 , qui correctement lu que lock3 était égal à 1, commits.

je end n'est pas pris, et mov var, 0 est exécuté.

dans le Thread 3, ebx est égal à 0. Cela aurait dû être impossible. C'est la violation de l'ordre de mémoire que Intel doit compenser.


maintenant, la solution Qu'Intel prend pour éviter ce comportement invalide, est de tirer la chasse. Quand lock3 = 0 a couru sur Thread 2, Il forces Thread 1 pour rincer les instructions d'utilisation lock3. Le rinçage dans ce cas signifie que Thread 1 n'ajoutera pas d'instructions au pipeline tant que toutes les instructions qui utilisent lock3 n'auront pas été engagées. Avant que le Thread 1 cmp lock3 puisse commettre, le cmp lock1 doit commettre. Quand le cmp lock1 essaie de commettre, il lit que lock1 est en fait égal à 1, et que la prédiction de la branche était un échec. Cela provoque le cmp d'être jeté. Maintenant que le fil 1 est rincé, lock3 's l'emplacement dans le cache du Thread 1 est défini à 0 , puis le Thread 1 continue l'exécution (en attendant lock1 ). Thread 2 est maintenant notifié que tous les autres noyaux ont vidé l'usage de lock3 et mis à jour leurs caches, donc le Thread 2 continue alors l'exécution (il aura exécuté des déclarations indépendantes dans l'intervalle, mais l'instruction suivante était une autre écriture donc il doit probablement être suspendu, à moins que les autres noyaux ont une queue pour tenir l'écriture lock1 = 0 en attente).

tout ce processus est coûteux, d'où la PAUSE. La PAUSE aide Thread 1, qui peut maintenant récupérer de la branche imminente fausse interprétation instantanément, et il ne doit pas rincer son pipeline avant de brancher correctement. La PAUSE aide également le Thread 2, qui n'a pas à attendre le rinçage du Thread 1 (Comme dit plus haut, Je ne suis pas sûr de ce détail d'implémentation, mais si le Thread 2 essaie d'écrire des serrures utilisées par trop d'autres noyaux, le Thread 2 devra éventuellement attendre flush.)

une compréhension importante est que bien que dans mon exemple, le flush est nécessaire, dans L'exemple de Mackie, il ne l'est pas. Cependant, le CPU n'a aucun moyen de le savoir (il n'analyse pas le code du tout, sauf en vérifiant les dépendances de déclaration consécutives, et un cache de prédiction de branche), donc le CPU va vider les instructions en accédant à lockvar dans L'exemple de Mackie comme il le fait dans le mien, afin de garantir l'exactitude.

3
répondu Nicholas Pipitone 2018-08-01 20:45:20