Question

J'ai lu le "Guide de Intel Optimisation pour l'architecture Intel".

Cependant, je ne sais toujours pas quand dois-je utiliser

_mm_sfence()
_mm_lfence()
_mm_mfence()

Quelqu'un pourrait-il expliquer quand ceux-ci devraient être utilisés lors de l'écriture du code multithread?

Était-ce utile?

La solution

caveat : Je ne suis pas expert dans ce domaine. Je suis encore en train d'apprendre moi-même. Mais puisque personne n'a répondu dans les deux derniers jours, il semble experts sur les instructions de clôture de la mémoire ne sont pas abondantes. Alors, voici ma compréhension ...

Intel est une système de mémoire faiblement ordonnée . Cela signifie que votre programme peut exécuter

array[idx+1] = something
idx++

mais le changement de idx peut être globalement visible (par exemple à des fils / processus en cours d'exécution sur les autres processeurs) avant le changement de array . Placer sfence entre les deux déclarations garantiront l'ordre les écritures sont envoyées au FSB.

Pendant ce temps, un autre processeur exécute

newestthing = array[idx]

peut-être mis en cache la mémoire pour array et a une copie rassis, mais obtient la mise à jour IDX en raison d'un manque de cache. La solution consiste à utiliser lfence juste à l'avance pour assurer les charges sont synchronisées.

Cet article ou cet article peut donner une meilleure information

Autres conseils

Voici ma compréhension, je l'espère assez précis et simple à faire sens:

(Itanium) l'architecture IA64 permet la mémoire lit et écrit à exécuter dans un ordre quelconque, de sorte que l'ordre des changements de mémoire du point de vue d'un autre processeur n'est pas prévisible à moins que vous utilisez des clôtures pour appliquer qui écrit complet dans un ordre raisonnable .

A partir de là, je parle x86, x86 est fortement ordonné.

Sur x86, Intel ne garantit pas qu'un magasin fait sur un autre processeur sera toujours immédiatement visible sur ce processeur. Il est possible que ce processeur spéculativement exécuté la charge (lecture), juste assez tôt pour manquer le magasin de l'autre processeur (écriture). Il garantit que l'ordre que les écritures deviennent visibles à d'autres processeurs est en ordre de programme. Il ne garantit pas que les autres processeurs verront immédiatement toute mise à jour, peu importe ce que vous faites.

Locked lecture / modify / instructions d'écriture sont entièrement compatibles de façon séquentielle. À cause de cela, en général, vous gérez déjà les opérations manquantes de la mémoire de l'autre processeur, car un xchg verrouillé ou cmpxchg se synchronisent tout, vous allez acquérir la ligne de cache pertinente pour la propriété immédiatement et mettra à jour atomiquement. Si une autre unité centrale de traitement est la course avec votre opération fermé à clé, que ce soit vous gagner la course et l'autre CPU va manquer le cache et le récupérer après votre opération verrouillée, ou ils vont gagner la course, et vous manquerez le cache et obtenir la mise à jour la valeur de leur part.

lfence stalles numéro d'instruction jusqu'à ce que toutes les instructions avant l'lfence sont terminées. mfence attend spécifiquement pour toute la mémoire précédente lit être mis pleinement dans le registre de destination, et attend toutes les écritures précédentes pour devenir globalement visible, mais ne bloque pas toutes les autres instructions lfence ferait. sfence fait la même chose que pour les magasins, les bouffées combineur écriture, et garantit que tous les magasins précédant la sfence sont globalement visibles avant d'autoriser les magasins suivant la sfence pour commencer l'exécution.

Les clôtures de toute nature sont rarement nécessaires sur x86, ils ne sont pas nécessaires sauf si vous utilisez la mémoire d'écriture combinant ou des instructions non-temporelles, quelque chose que vous faites rarement si vous n'êtes pas un développeur de mode noyau (pilote). Normalement, x86 garantit que tous les magasins sont visibles dans l'ordre du programme, mais il ne fait pas cette garantie pour les WC (combinaison d'écriture) mémoire ou pour obtenir des instructions « non-temporelles » qui ne stocke faiblement ordonnées explicites, comme movnti.

Donc, pour résumer, les magasins sont toujours visibles dans l'ordre du programme, sauf si vous avez spécial utilisé magasins faiblement commandés ou accédez type de mémoire WC. Les algorithmes utilisant des instructions verrouillées comme xchg, ou xadd, ou cmpxchg, etc., fonctionneront sans clôtures, car les instructions verrouillées sont cohérentes de façon séquentielle.

Si vous utilisez des magasins NT, vous voudrez peut-être ou peut-être même _mm_sfence _mm_mfence. Les cas d'utilisation pour _mm_lfence sont beaucoup plus obscurs.

Dans le cas contraire, il suffit d'utiliser C ++ 11 std :: atomique et laisser le souci du compilateur sur les détails asm de contrôle commande de la mémoire.


x86 a un modèle de mémoire fortement ordonnée, mais C ++ a un modèle de mémoire très faible (de même pour C). Pour la sémantique acquisition / release, il vous suffit de prévenir compilation réordonnancement . Voir Jeff Preshing de commande de mémoire à l'article de temps de compilation.

_mm_lfence et _mm_sfence ont l'effet compilateur barrière nécessaire, mais ils vont aussi entraîner le compilateur d'émettre un lfence inutile ou sfence instruction asm qui rend votre code plus lent.

Il y a de meilleures options pour contrôler réordonnancement compilation lorsque vous ne faites aucune des choses obscures qui vous voulez faire sfence.

Par exemple, asm("" ::: "memory") GNU C / C est une barrière de compilateur (toutes les valeurs doivent être en mémoire correspondant à la machine abstraite en raison de la clobber "memory"), mais aucune instruction asm sont émis.

Si vous utilisez C ++ 11 std :: atomique, vous pouvez tout simplement faire shared_var.store(tmp, std::memory_order_release). C'est garanti pour devenir globalement visible après toutes les affectations C antérieures, même à des variables non atomiques.

_mm_mfence est potentiellement utile si vous rouler votre propre version de C11 / C ++ 11 std::atomic , car une instruction mfence réelle est une façon d'obtenir la cohérence séquentielle , soit pour arrêter les charges ultérieures de lecture d'une valeur jusqu'à ce que les magasins précédentes deviennent visibles globalement. Voir Jeff Preshing mémoire Reordering dans la Loi sur Caught .

Mais notez que mfence semble être plus lent sur le matériel actuel que l'utilisation d'une opération atomique RMW verrouillée. par exemple. xchg [mem], eax est aussi une barrière complète, mais fonctionne plus rapidement et fait un magasin. Sur Skylake, la façon dont mfence est mis en œuvre empêche hors de l'ordre d'exécution de même instruction non-mémoire qui le suit. Voir bas de réponse .

En C ++, sans asm en ligne, cependant, vos options pour les barrières de mémoire sont plus limitées ( Combien d'instructions barrières de mémoire ne l'ont CPU x86? ). mfence est pas terrible, et il est ce que gcc et clang utilisent actuellement pour faire les magasins séquentiel cohérence.

Sérieusement il suffit d'utiliser C ++ 11 std :: atomique ou C11 stdatomic si possible, cependant; Il est plus facile à utiliser et vous obtenez tout à fait du bon code-gen pour beaucoup de choses. Ou dans le noyau Linux, il existe des fonctions déjà wrapper pour asm en ligne pour les barrières nécessaires. Parfois, c'est juste une barrière de compilateur, il est parfois aussi une instruction asm pour obtenir plus forte commande d'exécution que la valeur par défaut. (Par exemple pour une barrière complète).


barrières Pas feront vos magasins semblent autres threads plus vite. Tout ce qu'ils peuvent faire est de retarder les opérations ultérieures dans le thread courant jusqu'à ce que les choses se passent plus tôt. La CPU essaie déjà de commettre dans l'attente des magasins non spéculatifs à cache L1d le plus rapidement possible.


_mm_sfence est de loin l'obstacle le plus susceptible d'utiliser effectivement manuellement en C ++

Le principal cas d'utilisation pour _mm_sfence() est après quelques magasins _mm_stream, avant de fixer un drapeau que d'autres threads vérifieront.

Voir Enhanced REP MOVSB ?? memcpy pour plus sur les magasins NT par rapport aux magasins réguliers, et la bande passante de la mémoire x86. Pour l'écriture des tampons très grand (plus grand que L3 taille du cache) que définitivement ne sera pas Relisez dans un proche avenir, il peut être une bonne idée d'utiliser les magasins NT.

magasins NT sont faiblement commandés, contrairement à magasins normaux, vous avez donc besoin sfence si vous vous souciez de la publication des données à un autre fil. Dans le cas contraire (vous finirez par les lire à partir de ce fil), alors vous ne le faites pas. Ou si vous faites un appel système avant de dire à un autre thread les données sont prêtes, qui est également sérialisation.

sfence (ou une autre barrière) est nécessaire pour vous donner une libération / synchronisation lorsque vous utilisez acquérir des magasins NT. C ++ 11 implémentations std::atomic laissent à vous de clôturer vos magasins NT , de sorte que la libération-magasins atomiques peuvent être efficaces.

#include <atomic>
#include <immintrin.h>

struct bigbuf {
    int buf[100000];
    std::atomic<unsigned> buf_ready;
};

void producer(bigbuf *p) {
  __m128i *buf = (__m128i*) (p->buf);

  for(...) {
     ...
     _mm_stream_si128(buf,   vec1);
     _mm_stream_si128(buf+1, vec2);
     _mm_stream_si128(buf+2, vec3);
     ...
  }

  _mm_sfence();    // All weakly-ordered memory shenanigans stay above this line
  // So we can safely use normal std::atomic release/acquire sync for buf
  p->buf_ready.store(1, std::memory_order_release);
}

Ensuite, un consommateur peut faire en toute sécurité if(p->buf_ready.load(std::memory_order_acquire)) { foo = p->buf[0]; ... } sans données course comportement non défini. Le côté lecteur ne pas besoin _mm_lfence; la nature faiblement ordonnée de magasins NT se limite entièrement à cœur de faire l'écriture. Une fois qu'il devient visible à l'échelle mondiale, il est tout à fait cohérente et ordonnée selon les règles normales.

D'autres cas d'utilisation comprennent clflushopt de commande pour contrôler l'ordre des données étant stocké à une mémoire non volatile mappé en mémoire. (Par exemple un NVDIMM en utilisant la mémoire Optane, ou DIMM avec une batterie de secours DRAM existent maintenant.)


_mm_lfence est presque jamais utile comme une clôture de charge réelle . Les charges ne peuvent être faiblement ordonné lors du chargement de régions de mémoire WC (Write-Combining), comme RAM vidéo. Même movntdqa (_mm_stream_load_si128) est encore fortement ordonné sur la mémoire normale (WB = write-back), et ne fait rien pour réduire la pollution du cache. (prefetchnta pourrait, mais il est difficile de régler et peut faire empirer les choses.)

TL: DR. Si vous n'êtes pas en train d'écrire des pilotes graphiques ou quelque chose d'autre que des cartes RAM vidéo directement, vous n'avez pas besoin _mm_lfence de commander vos charges

lfence a le effet microarchitecture intéressant d'empêcher l'exécution des instructions ultérieures jusqu'à ce qu'il se retire. par exemple. pour arrêter _rdtsc() à la lecture du cycle en vente libre alors que le travail soit toujours en instance dans un microbenchmark. (S'applique toujours sur les processeurs Intel, mais AMD uniquement avec un paramètre MSR: Is LFENCE sérialisation sur processeurs AMD? . Sinon runs lfence 4 par cycle d'horloge sur la famille Bulldozer, clairement pas sérialisation.)

Puisque vous utilisez intrinsics de C / C ++, le compilateur génère le code pour vous. Vous ne disposez pas d'un contrôle direct sur l'asm, mais vous pourriez peut-être utiliser _mm_lfence pour des choses comme l'atténuation Specter si vous pouvez obtenir le compilateur de le mettre au bon endroit dans la sortie asm: juste après une branche conditionnelle, avant une double rangée accès. (Comme foo[bar[i]]). Si vous utilisez des correctifs du noyau pour Specter, je pense que le noyau défendra votre processus d'autres processus, de sorte que vous auriez seulement à vous soucier de cela dans un programme qui utilise un bac à sable JIT et est inquiet d'être attaqué à l'intérieur de son propre bac à sable.

Les appels intrinsèques que vous évoquez tous les il suffit d'insérer un sfence, lfence ou instruction mfence quand ils sont appelés . La question devient alors « Quels sont les objectifs de ces instructions de clôture »?

La réponse courte est que lfence est complètement inutile * et sfence presque complètement inutile à des fins de commande de mémoire pour les programmes en mode utilisateur en x86. D'autre part, mfence sert de barrière de mémoire pleine, vous pouvez l'utiliser dans des endroits où vous avez besoin d'une barrière s'il n'y a pas déjà une instruction préfixée de lock-proximité fournissant ce dont vous avez besoin.

Plus mais encore-réponse courte est ...

lfence

lfence est documenté à des charges de commande avant la lfence par rapport aux charges après, mais cette garantie est déjà prévu pour les charges normales sans aucune clôture du tout: qui est, Intel garantit déjà que « les charges ne sont pas réorganisés avec d'autres charges ». En pratique, cela laisse le but de lfence dans le code en mode utilisateur comme hors de l'ordre d'exécution barrière, peut-être utile pour certaines opérations soigneusement minutage.

sfence

sfence est documenté dans les magasins d'ordre avant et après de la même manière que pour les charges lfence fait, mais tout comme les charges de l'ordre de magasin est déjà garanti dans la plupart des cas par Intel. Le cas primaire intéressant où il n'est la soi-disant magasins non-temporelles telles que movntdq , movnti , maskmovq et quelques autres instructions. Ces instructions ne respectent pas les règles de commande de mémoire normale, de sorte que vous pouvez mettre un sfence entre ces magasins et les autres magasins où vous voulez appliquer l'ordre relatif. mfence travaille à cet effet aussi, mais sfence est plus rapide.

mfence

Contrairement aux deux autres, mfence ne fait quelque chose: elle sert de barrière de mémoire pleine, en veillant à ce que toutes les charges antérieures et les magasins auront complété 1 avant que les charges suivantes ou les magasins commencent exécution. Cette réponse est trop courte pour expliquer le concept d'une barrière de mémoire complètement, mais un exemple serait algorithme de Dekker , où chaque thread souhaitant entrer dans un magasin de section critique à un endroit, puis vérifie si l'autre thread a stocké quelque chose à son emplacement. Par exemple, sur le fil 1:

mov   DWORD [thread_1_wants_to_enter], 1  # store our flag
mov   eax,  [thread_2_wants_to_enter]     # check the other thread's flag
test  eax, eax
jnz   retry
; critical section

Ici, sur x86, vous avez besoin d'une barrière de mémoire entre le magasin (la première mov), et la charge (le deuxième mov), sinon chaque thread pourrait voir zéro quand ils ont lu le drapeau de l'autre parce que le modèle de mémoire x86 permet charges à être commandés avec des magasins plus tôt. Ainsi, vous pouvez insérer une barrière mfence comme suit pour restaurer la cohérence séquentielle et le comportement correct de l'algorithme:

mov   DWORD [thread_1_wants_to_enter], 1  # store our flag
mfence
mov   eax,  [thread_2_wants_to_enter]     # check the other thread's flag
test  eax, eax
jnz   retry
; critical section

Dans la pratique, vous ne voyez pas mfence autant que vous pouvez vous attendre, parce que x86 verrouillage préfixés instructions ont le même effet barrière pleine, et ceux-ci sont souvent / toujours (?) moins cher qu'un mfence.


1 par exemple, les charges auront été satisfaites et les magasins will sont devenus visibles globalement (bien qu'il serait mis en œuvre différemment tant que l'ordre wrt effet visible est « comme si » qui a eu lieu).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top