J'ai lu ceci article sur les opérations atomiques, et il mentionne que l'affectation d'entier 32 bits est atomique sur x86, tant que la variable est naturellement alignée.
Pourquoi l'alignement naturel assure-t-il l'atomicité?
L'alignement "naturel" signifie aligné sur sa propre largeur de caractère . Ainsi, le chargement/stockage ne sera jamais divisé sur un type de frontière plus large que lui-même (par exemple, page, ligne de cache ou taille de bloc encore plus étroite utilisée pour les transferts de données entre différents caches).
Les processeurs font souvent des choses comme l'accès au cache ou les transferts de ligne de cache entre les cœurs, par blocs de taille de 2, donc les limites d'alignement plus petites qu'une ligne de cache sont importantes. (Voir les commentaires de @ BeeOnRope ci-dessous). Voir aussi Atomicity sur x86 pour plus de détails sur la façon dont les CPU implémentent les charges atomiques ou les stockent en interne, et num ++ peut-il être atomique pour 'int num'? pour en savoir plus sur la façon dont les opérations RMW atomiques comme atomic<int>::fetch_add()
/lock xadd
sont implémentés en interne.
Tout d'abord, cela suppose que le int
est mis à jour avec une instruction de magasin unique, plutôt que d'écrire différents octets séparément. Cela fait partie de ce que std::atomic
Garantit, mais ce C ou C++ simple ne le fait pas. Ce sera normalement cependant. x86-64 System V ABI n'interdit pas aux compilateurs de rendre les accès aux variables int
non atomiques, même si cela nécessite que int
soit 4B avec un alignement par défaut de 4B. Par exemple, x = a<<16 | b
Pourrait compiler vers deux magasins 16 bits distincts si le compilateur le voulait.
Les races de données sont un comportement indéfini en C et C++, donc les compilateurs peuvent et supposent que la mémoire n'est pas modifiée de manière asynchrone. Pour le code garanti de ne pas casser, utilisez C11 stdatomic ou C++ 11 std :: atomic . Sinon, le compilateur gardera juste une valeur dans un registre au lieu de recharger chaque fois que vous le lisez, comme volatile
mais avec des garanties réelles et un support officiel de la norme linguistique.
Avant C++ 11, les opérations atomiques étaient généralement effectuées avec volatile
ou d'autres choses, et une bonne dose de "travaux sur les compilateurs qui nous intéressent", donc C++ 11 était un énorme pas en avant. Maintenant, vous n'avez plus à vous soucier de ce que fait un compilateur pour plain int
; utilisez simplement atomic<int>
. Si vous trouvez d'anciens guides parlant de l'atomicité de int
, ils sont probablement antérieurs à C++ 11.
std::atomic<int> shared; // shared variable (compiler ensures alignment)
int x; // local variable (compiler can keep it in a register)
x = shared.load(std::memory_order_relaxed);
shared.store(x, std::memory_order_relaxed);
// shared = x; // don't do that unless you actually need seq_cst, because MFENCE or XCHG is much slower than a simple store
Note: pour atomic<T>
Plus grand que ce que le CPU peut faire atomiquement (donc .is_lock_free()
est faux), voir Où est le verrou pour un std :: atomic? . int
et int64_t
/uint64_t
sont cependant sans verrouillage sur tous les principaux compilateurs x86.
Ainsi, nous avons juste besoin de parler du comportement d'un insn comme mov [shared], eax
.
TL; DR: Le x86 ISA garantit que les magasins et les charges naturellement alignés sont atomiques, jusqu'à 64 bits de large. Les compilateurs peuvent donc utiliser des magasins/charges ordinaires tant qu'ils s'assurent que std::atomic<T>
A un alignement naturel.
(Mais notez que i386 gcc -m32
Ne parvient pas à le faire pour les types C11 _Atomic
64 bits, en les alignant uniquement sur 4B, donc atomic_llong
N'est pas réellement atomique. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146#c4 ). g++ -m32
Avec std::atomic
Est très bien, au moins en g ++ 5 car https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65147 a été corrigé en 2015 par une modification de l'en-tête <atomic>
. Cela n'a cependant pas changé le comportement du C11.)
IIRC, il y avait des systèmes SMP 386, mais la sémantique actuelle de la mémoire n'a été établie qu'en 486. C'est pourquoi le manuel dit "486 et plus récent".
Extrait des "Intel® 64 et IA-32 Architectures Software Developer Manuals, volume 3", avec mes notes en italique . (voir aussi le x86 balise wiki pour les liens: versions actuelles de tous les volumes, ou lien direct vers page 256 du pdf vol3 de décembre 2015 )
Dans la terminologie x86, un "mot" est deux octets de 8 bits. 32 bits sont un double mot ou DWORD.
Section 8.1.1 Opérations atomiques garanties
Le processeur Intel486 (et les processeurs plus récents depuis) garantit que les opérations de mémoire de base suivantes seront toujours effectuées de manière atomique:
- Lire ou écrire un octet
- Lire ou écrire un mot aligné sur une limite de 16 bits
- Lire ou écrire un double mot aligné sur une limite de 32 bits (C'est une autre façon de dire "alignement naturel")
Ce dernier point que j'ai mis en gras est la réponse à votre question: ce comportement fait partie de ce qui est requis pour qu'un processeur soit un processeur x86 (c'est-à-dire une implémentation de l'ISA).
Le reste de la section fournit des garanties supplémentaires pour les nouveaux processeurs Intel: Pentium étend cette garantie à 64 bits .
Le processeur Pentium (et les processeurs plus récents depuis) garantit que les opérations de mémoire supplémentaires suivantes seront toujours effectuées de manière atomique:
- Lecture ou écriture d'un mot-clé aligné sur une frontière de 64 bits (par exemple chargement/stockage x87 d'un
double
, oucmpxchg8b
(qui était nouveau dans Pentium P5))- Accès 16 bits à des emplacements de mémoire non mis en cache qui tiennent dans un bus de données 32 bits.
La section poursuit en soulignant que les accès répartis sur les lignes de cache (et les limites de page) ne sont pas garantis atomiques, et:
"Une instruction x87 ou une instruction SSE qui accède à des données plus grandes qu'un quadruple mot peut être implémentée en utilisant plusieurs accès à la mémoire."
Ainsi, entier, x87 et MMX/SSE chargent/stockent jusqu'à 64b, même en mode 32 bits ou 16 bits (par exemple movq
, movsd
, movhps
, pinsrq
, extractps
, etc.) sont atomiques si les données sont alignées. gcc -m32
Utilise movq xmm, [mem]
Pour implémenter des charges atomiques 64 bits pour des choses comme std::atomic<int64_t>
. Clang4.0 -m32
Utilise malheureusement lock cmpxchg8b
bug 33109 .
Sur certains processeurs avec des chemins de données internes de 128b ou 256b (entre les unités d'exécution et L1, et entre différents caches), les charges/magasins vectoriels 128b et même 256b sont atomiques, mais ce n'est pas garanti par n'importe quel standard ou facilement interrogable au moment de l'exécution, malheureusement pour les compilateurs implémentant std::atomic<__int128>
ou 16B structs .
Si vous voulez atomique 128b sur tous les systèmes x86, vous devez utiliser lock cmpxchg16b
(Disponible uniquement en mode 64 bits). (Et il n'était pas disponible dans les CPU x86-64 de première génération. Vous devez utiliser -mcx16
Avec gcc/clang pour qu'ils puissent l'émettre .)
Même les processeurs qui effectuent en interne des chargements/magasins atomiques 128b peuvent présenter un comportement non atomique dans des systèmes multi-socket avec un protocole de cohérence qui fonctionne en plus petits morceaux: par ex. AMD Opteron 2435 (K10) avec des threads s'exécutant sur des sockets séparés, connectés avec HyperTransport .
Les manuels d'Intel et d'AMD divergent pour un accès non aligné à la mémoire pouvant être mise en cache . Le sous-ensemble commun à tous les processeurs x86 est la règle AMD. La mise en cache signifie les régions de mémoire réinscriptible ou réécrite, non non mise en cache ou combinaison d'écriture, comme défini avec les régions PAT ou MTRR. Ils ne signifient pas que la ligne de cache doit déjà être chaude dans le cache L1.
AMD garantit l'atomicité des charges/magasins pouvant être mis en cache qui tiennent dans un seul bloc aligné sur 8B. Cela a du sens, car nous savons du test 16B-store sur Opteron multi-socket qu'HyperTransport ne transfère que dans des morceaux 8B, et ne se verrouille pas pendant le transfert pour empêcher la déchirure. (Voir au dessus). Je suppose que lock cmpxchg16b
Doit être traité spécialement.
Peut-être lié: AMD utilise MOESI pour partager des lignes de cache sales directement entre les caches dans différents cœurs, de sorte qu'un cœur peut lire à partir de sa copie valide d'une ligne de cache tandis que les mises à jour proviennent d'un autre cache.
Intel utilise MESIF , ce qui nécessite des données sales pour se propager vers le grand cache L3 inclusif partagé qui agit comme un filet de sécurité pour le trafic de cohérence. L3 inclut les balises des caches L2/L1 par cœur, même pour les lignes qui doivent être à l'état non valide dans L3 en raison de leur statut M ou E dans un cache L1 par cœur. Le chemin de données entre L3 et les caches par cœur n'a qu'une largeur de 32B dans Haswell/Skylake, il doit donc mettre en mémoire tampon ou quelque chose pour éviter qu'une écriture sur L3 à partir d'un cœur ne se produise entre les lectures de deux moitiés d'une ligne de cache, ce qui pourrait provoquer une déchirure à la limite 32B.
Les sections pertinentes des manuels:
Les processeurs de la famille P6 (et les processeurs Intel plus récents depuis) garantissent que l'opération de mémoire supplémentaire suivante sera toujours effectuée de manière atomique:
- Accès non alignés de 16, 32 et 64 bits à la mémoire en cache qui tiennent dans une ligne de cache.
Manuel AMD64 7.3.2 Atomicité d'accès
Les charges ou les mémoires simples alignables naturellement pouvant être mises en cache jusqu'à un mot quadruple sont atomiques sur tout modèle de processeur, tout comme les charges ou les mémoires mal alignées inférieures à un mot quadruple qui sont entièrement contenues dans un mot quadruple aligné naturellement.
Notez qu'AMD garantit l'atomicité pour toute charge inférieure à un qword, mais Intel uniquement pour les tailles de puissance de 2. Le mode protégé 32 bits et le mode long 64 bits peuvent charger un m16:32
48 bits comme opérande mémoire dans cs:eip
Avec far -call
ou far -jmp
. (Et les appels lointains poussent des trucs sur la pile.) IDK si cela compte comme un accès 48 bits ou 16 et 32 bits séparés.
Il y a eu des tentatives pour formaliser le modèle de mémoire x86, le dernier étant le papier x86-TSO (version étendue) de 2009 (lien de la section de commande de mémoire du x86 = tag wiki). Ce n'est pas utile à parcourir car ils définissent certains symboles pour exprimer les choses dans leur propre notation, et je n'ai pas vraiment essayé de le lire. IDK s'il décrit les règles d'atomicité, ou s'il ne concerne que l'ordre de la mémoire .
J'ai mentionné cmpxchg8b
, Mais je ne parlais que de la charge et du magasin chacun étant atomique (c.-à-d. Pas de "déchirure" où la moitié de la charge provient d'un magasin, l'autre moitié de la charge provient d'un magasin différent).
Pour éviter que le contenu de cet emplacement mémoire ne soit modifié entre la charge et le magasin, vous avez besoin lock
cmpxchg8b
, Tout comme vous avez besoin de lock inc [mem]
Pour que toute la lecture-modification-écriture soit atomique. Notez également que même si cmpxchg8b
Sans lock
effectue une seule charge atomique (et éventuellement un magasin), il n'est généralement pas sûr de l'utiliser comme une charge 64b avec la valeur attendue = souhaitée. Si la valeur en mémoire correspond à votre attente, vous obtiendrez une lecture-modification-écriture non atomique de cet emplacement.
Le préfixe lock
rend atomiques même les accès non alignés qui traversent les lignes de cache ou les limites de page, mais vous ne pouvez pas l'utiliser avec mov
pour créer un magasin non aligné ou charger atomique. Il n'est utilisable qu'avec des instructions de lecture-modification-écriture de destination mémoire comme add [mem], eax
.
(lock
est implicite dans xchg reg, [mem]
, donc n'utilisez pas xchg
avec mem pour enregistrer la taille du code ou le nombre d'instructions à moins que les performances ne soient pas pertinentes. Utilisez-le uniquement lorsque vous veulent la barrière mémoire et/ou l'échange atomique, ou quand la taille du code est la seule chose qui compte, par exemple dans un secteur de démarrage.)
Voir aussi: num ++ peut-il être atomique pour 'int num'?
lock mov [mem], reg
N'existe pas pour les magasins atomiques non alignésDans le manuel de référence insn (Intel x86 manual vol2), cmpxchg
:
Cette instruction peut être utilisée avec un préfixe
LOCK
pour permettre à l'instruction d'être exécutée atomiquement. Pour simplifier l'interface avec le bus du processeur, l'opérande de destination reçoit un cycle d'écriture indépendamment du résultat de la comparaison. L'opérande de destination est réécrit si la comparaison échoue; sinon, l'opérande source est écrit dans la destination. ( Le processeur ne produit jamais une lecture verrouillée sans produire également une écriture verrouillée .)
Cette décision de conception a réduit la complexité du chipset avant que le contrôleur de mémoire ne soit intégré au CPU. Il peut toujours le faire pour les instructions lock
ed sur les régions MMIO qui frappent le bus PCI-express plutôt que la DRAM. Il serait tout simplement déroutant pour un lock mov reg, [MMIO_PORT]
De produire une écriture ainsi qu'une lecture dans le registre d'E/S mappé en mémoire.
L'autre explication est qu'il n'est pas très difficile de s'assurer que vos données ont un alignement naturel, et lock store
Fonctionnerait horriblement par rapport à la simple vérification de l'alignement de vos données. Il serait stupide de dépenser des transistors pour quelque chose qui serait si lent qu'il ne serait pas utile de l'utiliser. Si vous en avez vraiment besoin (et que cela ne vous dérange pas de lire la mémoire également), vous pouvez utiliser xchg [mem], reg
(XCHG a un préfixe LOCK implicite), ce qui est encore plus lent qu'un hypothétique lock mov
.
L'utilisation d'un préfixe lock
est également une barrière de mémoire complète, de sorte qu'elle impose une surcharge de performances au-delà du simple RMW atomique. c'est-à-dire que x86 ne peut pas faire RMW atomique détendu (sans vider le tampon de stockage). D'autres ISA le peuvent, donc l'utilisation de .fetch_add(1, memory_order_relaxed)
peut être plus rapide sur les non-x86.
Fait amusant: avant que mfence
n'existait, un idiome commun était lock add dword [esp], 0
, Qui est un no-op autre que des drapeaux clobber et une opération verrouillée. [esp]
Est presque toujours actif dans le cache L1 et ne causera pas de conflit avec un autre noyau. Cet idiome peut être encore plus efficace que MFENCE en tant que barrière de mémoire autonome, en particulier sur les processeurs AMD.
xchg [mem], reg
Est probablement le moyen le plus efficace d'implémenter un magasin de cohérence séquentielle, contre mov
+ mfence
, sur Intel et AMD. mfence
sur Skylake bloque au moins l'exécution dans le désordre des instructions non-mémoire, mais xchg
et d'autres lock
ed ops ne le font pas. Les compilateurs autres que gcc utilisent xchg
pour les magasins, même lorsqu'ils ne se soucient pas de lire l'ancienne valeur.
Sans cela, le logiciel devrait utiliser des verrous à 1 octet (ou une sorte de type atomique disponible) pour protéger les accès aux entiers 32 bits, ce qui est extrêmement inefficace par rapport à l'accès en lecture atomique partagé pour quelque chose comme une variable d'horodatage globale mise à jour par une interruption de temporisation . Il est probablement fondamentalement libre de silicium pour garantir des accès alignés de largeur de bus ou plus petits.
Pour que le verrouillage soit possible, une sorte d'accès atomique est nécessaire. (En fait, je suppose que le matériel pourrait fournir une sorte de mécanisme de verrouillage assisté par matériel totalement différent.) Pour un processeur qui effectue des transferts 32 bits sur son bus de données externe, il est logique que ce soit l'unité d'atomicité.
Depuis que vous avez offert une prime, je suppose que vous cherchiez une longue réponse qui a erré dans tous les sujets secondaires intéressants. Faites-moi savoir s'il y a des choses que je n'ai pas couvertes qui, selon vous, rendraient ce Q&R plus précieux pour les futurs lecteurs.
Puisque vous en avez lié un dans la question , Je recommande fortement de lire davantage de billets de blog de Jeff Preshing . Ils sont excellents et m'ont aidé à rassembler les éléments de ce que je savais dans une compréhension de l'ordre de la mémoire en source C/C++ vs asm pour différentes architectures matérielles, et comment/quand dire au compilateur ce que vous voulez si vous n'êtes pas '' t écrivant directement asm.
Si un objet 32 bits ou plus petit est naturellement aligné dans une partie "normale" de la mémoire, il sera possible pour n'importe quel processeur 80386 ou compatible autre que le 80386sx de lire ou d'écrire tous les 32 bits de l'objet en une seule opération. Bien que la capacité d'une plate-forme à faire quelque chose d'une manière rapide et utile ne signifie pas nécessairement que la plate-forme ne le fera pas parfois d'une autre manière pour une raison quelconque, et même si je pense qu'il est possible sur de nombreux sinon tous les processeurs x86 de ont des régions de mémoire qui ne sont accessibles que sur 8 ou 16 bits à la fois, je ne pense pas qu'Intel ait jamais défini de conditions dans lesquelles la demande d'un accès 32 bits aligné à une zone de mémoire "normale" entraînerait la lecture du système ou écrire une partie de la valeur sans lire ou écrire le tout, et je ne pense pas qu'Intel ait l'intention de définir une telle chose pour des zones de mémoire "normales".
Naturellement aligné signifie que l'adresse du type est un multiple de la taille du type.
Par exemple, un octet peut être à n'importe quelle adresse, un court (en supposant 16 bits) doit être sur un multiple de 2, un int (en supposant 32 bits) doit être sur un multiple de 4 et un long (en supposant 64 bits) doit être sur un multiple de 8.
Si vous accédez à un élément de données qui n'est pas naturellement aligné, le processeur soulèvera une erreur ou lira/écrit la mémoire, mais pas comme une opération atomique. L'action du processeur dépendra de l'architecture.
Par exemple, l'image nous avons la disposition de la mémoire ci-dessous:
01234567
...XXXX.
et
int *data = (int*)3;
Lorsque nous essayons de lire *data
les octets qui composent la valeur sont répartis sur 2 blocs de taille int, 1 octet est dans le bloc 0-3 et 3 octets sont dans le bloc 4-7. Maintenant, ce n'est pas parce que les blocs sont logiquement côte à côte qu'ils sont physiquement. Par exemple, le bloc 0-3 pourrait être à la fin d'une ligne de cache de processeur, tandis que le bloc 3-7 se trouve dans un fichier d'échange. Lorsque le processeur accède au bloc 3-7 afin d'obtenir les 3 octets dont il a besoin, il peut voir que le bloc n'est pas en mémoire et signale qu'il a besoin de la mémoire paginée. Cela bloquera probablement le processus d'appel pendant que le système d'exploitation pages la mémoire en arrière.
Une fois que la mémoire a été paginée, mais avant que votre processus ne soit réveillé, un autre peut venir et écrire un Y
à l'adresse 4. Ensuite, votre processus est replanifié et le CPU termine la lecture, mais maintenant il a lisez XYXX, plutôt que le XXXX que vous attendiez.
Si vous demandez pourquoi il est conçu de cette façon, je dirais que c'est un bon produit secondaire de la conception de l'architecture du processeur.
À l'époque 486, il n'y a pas de processeur multicœur ou de liaison QPI, donc l'atomicité n'est pas vraiment une exigence stricte à ce moment-là (DMA peut l'exiger?).
Sur x86, la largeur des données est de 32 bits (ou 64 bits pour x86_64), ce qui signifie que le processeur peut lire et écrire jusqu'à la largeur des données en une seule fois. Et le bus de données mémoire est généralement identique ou plus large que ce nombre. Combiné avec le fait que la lecture/écriture sur une adresse alignée se fait en une seule fois, naturellement rien n'empêche la lecture/écriture d'être non atomique. Vous gagnez en vitesse/atomique en même temps.
Pour répondre à votre première question, une variable est naturellement alignée si elle existe à une adresse mémoire qui est un multiple de sa taille.
Si nous considérons uniquement - comme l'article que vous avez lié - les instructions d'affectation , l'alignement garantit l'atomicité parce que MOV (l'instruction d'affectation) est atomique par conception sur aligné Les données.
D'autres types d'instructions, INC par exemple, doivent être LOCK ed (un préfixe x86 qui donne un accès exclusif à la mémoire partagée au processeur actuel pendant la durée de l'opération préfixée) même si les données sont alignées car elles s'exécutent en fait en plusieurs étapes (= instructions, à savoir charger, inc, stocker).