C++11 a introduit un modèle de mémoire standardisé. Ça veut dire quoi? Et comment cela va-t-il affecter la programmation C++?

C++11 a introduit un modèle de mémoire standardisé, mais qu'est-ce que cela signifie exactement? Et comment cela va-t-il affecter la programmation C++?

Cet article (par Gavin Clarke qui cite Herb Sutter ) qui dit,

le modèle de mémoire signifie que le code C++ a maintenant une bibliothèque standardisée à appeler peu importe qui fait le compilateur et sur quelle plateforme c'est exécuter. Il y a un moyen standard de contrôler comment différents fils de parler à la la mémoire du processeur.

"Quand vous parlez de fractionnement [code] dans les différents noyaux c'est dans la norme, nous parlons d' le modèle de mémoire. Nous allons optimisez-le sans briser le suivant les hypothèses les gens vont pour faire dans le code," Sutter dit.

bien, je peux mémoriser CE et des paragraphes similaires disponibles en ligne (comme j'ai eu mon propre modèle de mémoire depuis la naissance :P) et peut même poster comme réponse aux questions posées par d'autres, mais pour être honnête, je ne comprends pas exactement cela.

donc, ce que je veux savoir fondamentalement, c'est que les programmeurs C++ ont utilisé pour développer des applications multi-thread même avant, alors quelle importance cela a-t-il si C'est des threads POSIX, ou des threads Windows, ou des threads C++11? Quels sont les avantages? Je veux comprendre la détails de bas niveau.

j'ai aussi le sentiment que le modèle mémoire C++11 est en quelque sorte lié au support multi-threading C++11, Comme je les vois souvent ensemble. Si elle l'est, comment exactement? Pourquoi devraient-ils être liés?

comme je ne sais pas comment fonctionnent les internes de multi-threading, et ce que le modèle de mémoire signifie en général, s'il vous plaît aidez-moi à comprendre ces concepts. :- )

1585
demandé sur KIN 2011-06-12 03:30:14
la source

6 ответов

tout d'abord, vous devez apprendre à penser comme un avocat de langue.

la spécification C++ ne fait référence à aucun compilateur, système d'exploitation ou CPU particulier. Il fait référence à une machine abstraite qui est une généralisation de systèmes réels. Dans le monde du droit du langage, le travail du programmeur est d'écrire du code pour la machine abstraite; le travail du compilateur est d'actualiser ce code sur une machine concrète. Par en codant rigidement selon les spécifications, vous pouvez être certain que votre code compilera et fonctionnera sans modification sur n'importe quel système avec un compilateur C++ compatible, que ce soit aujourd'hui ou dans 50 ans.

la machine abstraite de la spécification C++98/C++03 est fondamentalement monofilée. Il n'est donc pas possible d'écrire du code C++ multi-threadé "entièrement portable" par rapport à la spécification. La spécification ne dit même rien au sujet du atomicité de les charges de mémoire et les magasins ou le ordre dans lequel les charges et les magasins pourraient se produire, sans parler des choses comme les Mutex.

bien sûr, vous pouvez écrire du code multi-threadé dans la pratique pour des systèmes de béton particuliers -- comme pthreads ou Windows. Mais il n'y a pas de standard façon d'écrire du code multi-threadé pour C++98/C++03.

la machine abstraite en C++11 est multi-filetée par conception. Il dispose également d'un bien défini modèle de mémoire , c'est-à-dire qu'il indique ce que le compilateur peut et ne peut pas faire lorsqu'il s'agit d'accéder à la mémoire.

considérons l'exemple suivant, où une paire de variables globales est accédée simultanément par deux threads:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

Qu'est-ce que Thread 2 sortie?

sous C++98 / C++03, Ce N'est même pas un comportement indéfini; la question elle-même est sans signification parce que la norme ne considère rien appelé un "fil".

sous C++11, le résultat est un comportement non défini, parce que les charges et les stocks n'ont pas besoin d'être atomiques en général. Qui peut ne pas sembler beaucoup d'amélioration... Et par lui-même, il n'est pas.

mais avec C++11, Vous pouvez écrire ceci:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Maintenant, les choses deviennent beaucoup plus intéressantes. Tout d'abord, le comportement ici est défini . Le Thread 2 peut maintenant imprimer 0 0 (s'il court avant le Thread 1), 37 17 (s'il court après le Thread 1), ou 0 17 (s'il court après le Thread 1).

ce qu'il ne peut pas imprimer est 37 0 , parce que le mode par défaut pour les charges atomiques/magasins en C++11 est d'appliquer consistance séquentielle . Cela signifie Juste que tous les chargements et les magasins doivent être" comme si " ils se sont produits dans l'ordre où vous les avez écrits chaque thread, tandis que les opérations parmi les threads peuvent être entrelacés cependant le système aime. Ainsi, le comportement par défaut d'atomics fournit à la fois atomicité et commande pour les charges et les magasins.

maintenant, sur un CPU moderne, assurer la cohérence séquentielle peut être coûteux. En particulier, le compilateur est susceptible d'émettre des barrières mémoire complètes entre chaque accès ici. Mais si votre algorithme peut tolérer de charges et stocke; c.-à-d., si elle exige atomicité mais pas l'ordre; c.-à-d., si elle peut tolérer 37 0 comme sortie de ce programme, alors vous pouvez écrire ceci:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

plus le CPU est moderne, plus il est probable que ce soit plus rapide que dans l'exemple précédent.

enfin, si vous avez juste besoin de garder des charges et des stocks particuliers dans l'ordre, vous pouvez écrire:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

cela nous ramène aux chargements et magasins commandés -- donc 37 0 n'est plus une sortie possible -- mais il le fait avec un minimum de frais généraux. (Dans cet exemple trivial, le résultat est le même que la cohérence séquentielle complète; dans un programme plus grand, il ne le serait pas.)

bien sûr, si les seules sorties que vous voulez voir sont 0 0 ou 37 17 , vous pouvez simplement enrouler un mutex autour du code original. Mais si vous avez lu si loin, je parie que vous savez déjà comment cela fonctionne, et cette réponse est déjà plus long que je destiné.)-:

Donc, la ligne du bas. Les mutex sont super, et C++11 les standardise. Mais parfois, pour des raisons de performance, vous voulez des primitives de niveau inférieur (par exemple, le classique double-vérifié schéma de verrouillage ). La nouvelle norme fournit des gadgets de haut niveau comme les mutex et les variables de condition, et il fournit également des gadgets de bas niveau comme les types atomiques et les saveurs diverses de la barrière de mémoire. Donc maintenant vous pouvez écrire sophistiqué, haute performance routines concurrentes entièrement dans la langue spécifiée par la norme, et vous pouvez être certain que votre code sera compilé et exécuté sans changement sur les systèmes d'aujourd'hui et de demain.

bien que pour être franc, à moins que vous ne soyez un expert et que vous travailliez sur un code de bas niveau sérieux, vous devriez probablement vous en tenir aux mutex et aux variables de condition. C'est ce que j'ai l'intention de faire.

pour plus d'informations sur ce truc, voir ce billet de blog .

1839
répondu Nemo 2017-11-06 06:35:04
la source

je vais juste donner l'analogie avec laquelle je comprends les modèles de consistance de mémoire (ou modèles de mémoire, pour faire court). Il est inspiré par Leslie Lamport séminal de papier "du Temps, les Horloges, et l'ordre des Événements dans un Système Distribué" . L'analogie est pertinente et a une signification fondamentale, mais peut être exagérée pour beaucoup de gens. Cependant, j'espère qu'il fournit une image mentale (une représentation picturale) qui facilite le raisonnement au sujet des modèles de cohérence de la mémoire.

voyons les histoires de tous les emplacements de mémoire dans un diagramme espace-temps dans lequel l'axe horizontal représente l'espace d'adresse (c.-à-d., chaque emplacement de mémoire est représenté par un point sur cet axe) et l'axe vertical représente le temps (nous verrons que, en général, il n'y a pas une notion universelle du temps). L'histoire de valeurs détenues par chaque emplacement de mémoire est donc représentée par une colonne verticale à l'adresse mémoire. Chaque changement de valeur est dû à l'un des threads écrivant une nouvelle valeur à cet endroit. Par image mémoire , nous entendons l'agrégat/la combinaison de valeurs de tous les emplacements mémoire observables à un moment particulier 151980920 "par 151960920" un fil particulier 151980920".

citation de "une introduction sur la cohérence de la mémoire et de la mémoire Cache "

le modèle de mémoire intuitif (et le plus restrictif) est la consistance séquentielle (SC) dans laquelle une exécution multithreaded devrait ressembler à un entrelacement des exécutions séquentielles de chaque thread constitutif, comme si les threads étaient multiplexés dans le temps sur un processeur à un seul cœur.

que l'ordre de mémoire globale peut varier d'une exécution du programme à l'autre et peut ne pas être connu à l'avance. La caractéristique de SC est l'ensemble des tranches du diagramme adresse-espace-temps représentant plans de simultanéité 151980920 " (c.-à-d. images de mémoire). Sur un plan donné, tous ses événements (ou valeurs de mémoire) sont simultanés. Il y a une notion de temps absolu , dans laquelle tous les fils s'accordent sur les valeurs de mémoire qui sont simultanées. En SC, à chaque instant, il n'y a qu'une seule image mémoire partagée par tous les threads. C'est, à chaque instant, tous les processeurs sont d'accord sur l'image mémoire (c.-à-d. le contenu agrégé de la mémoire). Non seulement cela implique que tous les threads voient la même séquence de valeurs pour tous les emplacements de mémoire, mais aussi que tous les processeurs observent les mêmes combinaisons de valeurs de toutes les variables. C'est la même chose que de dire que toutes les opérations de mémoire (sur tous les emplacements de mémoire) sont observées dans le même ordre total par tous les threads.

dans les modèles de mémoire décontractée, chaque fil découpera l'adresse-espace-temps à sa manière, la seule restriction étant que les tranches de chaque fil ne doivent pas se croiser parce que tous les fils doivent s'entendre sur l'histoire de chaque emplacement de mémoire individuelle (Bien sûr, les tranches de différents fils peuvent, et vont, se croiser). Il n'y a pas de moyen universel de le découper (pas de foliation privilégiée d'adresse-espace-temps). Les tranches ne doivent pas être planes (ou linéaires). Ils peuvent être courbés et c'est ce qui peut faire un thread lire des valeurs écrites par un autre thread hors de l'ordre ils ont été écrits. Des histoires de lieux de mémoire différents peuvent glisser (OU être étirées) arbitrairement par rapport à l'autre lorsqu'on les regarde par un fil particulier . Chaque fil aura un sens différent des événements (ou, de façon équivalente, des valeurs de mémoire) qui sont simultanés. L'ensemble des événements (ou des valeurs de mémoire) qui sont simultanés à un thread ne sont pas simultanés à un autre. Ainsi, dans un modèle de mémoire détendue, tous les fils observent toujours le même l'historique (c.-à-d. la séquence des valeurs) pour chaque emplacement de mémoire. Mais ils peuvent observer différentes images de mémoire (i.e., combinaisons de valeurs de tous les emplacements de mémoire). Même si deux emplacements de mémoire différents sont écrits par le même thread dans l'ordre, les deux valeurs nouvellement écrites peuvent être observées dans un ordre différent par d'autres threads.

[image de Wikipedia] Picture from Wikipedia

lecteurs familiers avec la théorie spéciale de Einstein La relativité remarquera ce à quoi je fais allusion. Traduire les mots de Minkowski dans le royaume des modèles de mémoire: l'espace et le temps d'adresse sont des ombres d'adresse-espace-temps. Dans ce cas, chaque observateur (i.e., fil) projettera des ombres d'événements (i.e., mémoires/charges) sur sa propre ligne du monde (i.e., son axe du temps) et son propre plan de simultanéité (son axe adresse-espace). Les Threads dans le modèle de mémoire C++11 correspondent à observateurs qui se déplacent l'un par rapport à l'autre dans la relativité spéciale. La cohérence séquentielle correspond au espace-temps Galiléen (c'est-à-dire que tous les observateurs s'accordent sur un ordre absolu d'événements et un sens global de simultanéité).

la ressemblance entre les modèles de mémoire et la relativité spéciale provient du fait que les deux définissent un ensemble d'événements partiellement ordonnés, souvent appelé un ensemble causal. Certains événements (par exemple, de mémoire) peut affecter (mais ne pas être touché par) d'autres événements. Un fil C++11 (ou observateur en physique) n'est rien de plus qu'une chaîne (c'est-à-dire un ensemble totalement ordonné) d'événements (par exemple, des charges de mémoire et des stockages à des adresses éventuellement différentes).

en relativité, un certain ordre est restauré à l'image apparemment chaotique d'événements partiellement ordonnés, puisque le seul ordre temporel sur lequel tous les observateurs s'accordent est l'ordre parmi les événements "temporels" (i.e., ces événements qui sont en principe connectables par n'importe quelle particule allant plus lent que la vitesse de la lumière dans le vide). Seuls les événements liés au temps sont invariablement ordonnés. le Temps dans la Physique, Craig Callender .

dans le modèle de mémoire C++11, un mécanisme similaire (le modèle de cohérence acquisition-libération) est utilisé pour établir ces relations de causalité locale .

pour fournir une définition de la consistance de la mémoire et une motivation pour abandonner SC, je vais citer de "Un Apprêt sur la Mémoire de la Consistance et de la Cohérence de Cache"

pour une machine à mémoire partagée, le modèle de consistance de mémoire définit le comportement architectural visible de son système de mémoire. Le critère d'exactitude pour un comportement de partitions de coeur de processeur unique entre " un résultat correct "et" beaucoup d'alternatives incorrectes ". Cela est dû au fait que l'architecture du processeur que l'exécution d'un thread transforme un État d'entrée donné en un seul État de sortie bien défini, même sur un noyau hors-ordre. Les modèles de cohérence de mémoire partagée, cependant, concernent les charges et les stocks de fils multiples et permettent habituellement de nombreuses exécutions correctes tout en rejetant beaucoup (plus) incorrectes. La possibilité de plusieurs exécutions correctes est due à L'ISA permettant à plusieurs threads d'exécuter simultanément, souvent avec de nombreuses possibilités juridiques interleavings d'instructions de différents fils.

Détendu ou faible la mémoire de la cohérence des modèles sont motivés par le fait que la plupart de la mémoire de rangements dans les modèles forts sont inutiles. Si un thread met à jour dix éléments de données et ensuite un drapeau de synchronisation, les programmeurs ne se soucient généralement pas si les éléments de données sont mis à jour dans l'ordre les uns par rapport aux autres, mais seulement que tous les éléments de données sont mis à jour avant la mise à jour du drapeau (habituellement mis en œuvre à l'aide des instructions de clôture). Les modèles détendus cherchent à capturer cette plus grande flexibilité de commande et de préserver seulement les commandes que les programmeurs " exigent " pour obtenir à la fois des performances plus élevées et la justesse de SC. Par exemple, dans certaines architectures, des tampons d'écriture FIFO sont utilisés par chaque noyau pour contenir les résultats des magasins engagés (retirés) avant d'écrire les résultats aux caches. Cette optimisation améliore la performance mais viole SC. Le tampon d'écriture cache la latence de l'entretien d'un magasin manqué. Parce que les magasins sont communs, être en mesure d'éviter de gagner du temps sur la plupart d'entre eux est un avantage important. Pour un processeur à noyau simple, un tampon d'écriture peut être rendu invisible sur le plan architectural en s'assurant qu'une charge pour adresser A renvoie la valeur de la mémoire la plus récente à A même si une ou plusieurs mémoires à A se trouvent dans le tampon d'écriture. Cela se fait généralement en contournant la valeur du magasin le plus récent à A à la charge de A, où "la plus récente" est déterminée par ordre de programme, ou en bloquant une charge de A si un stock à A est dans le tampon d'écriture. Lorsque plusieurs cœurs sont utilisés, chacun aura son propre tampon d'écriture de contournement. Sans tampon d'écriture, le matériel est SC, mais avec tampon d'écriture, il ne l'est pas, ce qui rend les tampons d'écriture architectoniquement visibles dans un processeur multicore.

Magasin de magasin de réorganisation peut se produire si un noyau non-FIFO mémoire tampon d'écriture qui permet de magasins de départ dans un ordre différent de l'ordre dans lequel ils sont entrés. Cela peut se produire si le premier magasin manque dans la cache pendant que le deuxième magasin frappe ou si le deuxième magasin peut fusionner avec un magasin plus tôt (c.-à-d., avant le premier magasin). Le réordonnement charge-charge peut aussi se produire sur des noyaux programmés dynamiquement qui exécutent des instructions hors de l'ordre du programme. Qui peut se comporter de la même façon que réorganiser des magasins sur un autre noyau (pouvez-vous trouver un exemple entrelaçant deux fils?). Réorganiser un plus tôt la charge avec un stockage ultérieur (un réarrangement load-store) peut causer de nombreux comportements incorrects, tels que le chargement d'une valeur après avoir libéré le verrou qui le protège (si le stockage est l'opération de déverrouillage). Notez que les réorganisations de stock-load peuvent également se produire en raison d'un contournement local dans le tampon D'écriture FIFO couramment mis en œuvre, même avec un noyau qui exécute toutes les instructions dans l'ordre du programme.

parce que la cohérence de cache et la cohérence de mémoire sont parfois confondues, il est instructif d'avoir aussi cette citation:

contrairement à la cohérence, cohérence de cache n'est ni visible pour le logiciel ni nécessaire. La cohérence cherche à rendre les caches d'un système à mémoire partagée aussi fonctionnellement invisibles que les caches d'un système à noyau unique. Une cohérence correcte garantit qu'un programmeur ne peut pas déterminer si et où un système a des caches en analysant les résultats des charges et des stocks. C'est parce que la cohérence correcte garantit que les caches n'activent jamais le comportement " fonctionnel "(les programmeurs peuvent encore être en mesure de déduire la structure probable du cache en utilisant " informations de synchronisation ). Le but principal des protocoles de cohérence de cache est de maintenir l'invariant auteur simple-lecteurs multiples (SWMR) pour chaque emplacement de mémoire. Une distinction importante entre cohérence et cohérence est cette cohérence est spécifiée sur une base de par emplacement mémoire , tandis que la cohérence est spécifiée en ce qui concerne les emplacements mémoire " tous .

continuant avec notre image mentale, l'invariant SWMR correspond à l'exigence physique qu'il y ait au plus une particule située à un endroit quelconque mais il peut y avoir un nombre illimité d'observateurs de n'importe quel emplacement.

291
répondu Ahmed Nassar 2017-02-08 17:44:04
la source

c'est maintenant une question vieille de plusieurs années, mais étant très populaire, il est intéressant de mentionner une ressource fantastique pour apprendre sur le modèle de mémoire C++11. Je ne vois pas l'intérêt de résumer son exposé pour en faire une autre réponse complète, mais étant donné que c'est le gars qui a écrit le standard, je pense que ça vaut la peine de regarder le discours.

Herb Sutter a une discussion de trois heures sur le modèle de mémoire C++11 intitulé " atomic<> Les armes", disponible sur le site de channel 9 - partie 1 et partie 2 . L'exposé est assez technique, et couvre les sujets suivants:

  1. Optimisations, des Courses, et le Modèle de Mémoire
  2. commander-quoi: acquérir et libérer
  3. Commande – Comment: Mutex, Atomics, et/ou des Clôtures
  4. autres Restrictions sur les Compilateurs et le matériel
  5. code Gen& Performance: x86/ x64, IA64, POWER, ARM
  6. Relaxed Atomics

L'exposé ne développe pas sur L'API, mais plutôt sur le raisonnement, le fond, sous le capot et dans les coulisses (saviez-vous que la sémantique détendue a été ajoutée à la norme uniquement parce que la puissance et le bras ne supportent pas efficacement la charge synchronisée?).

85
répondu eran 2017-11-06 02:17:25
la source

cela signifie que la norme définit maintenant le multi-threading, et elle définit ce qui se passe dans le contexte des threads multiples. Bien sûr, les gens ont utilisé diverses implémentations, mais c'est comme demander pourquoi nous devrions avoir un std::string alors que nous pourrions tous utiliser un string roulé à la maison.

quand vous parlez de threads POSIX ou de threads Windows, alors c'est un peu une illusion car en fait vous parlez de threads x86, car c'est une fonction matérielle pour s'exécuter simultanément. Le modèle mémoire C++0x offre des garanties, que vous soyez sur x86, ARM, ou MIPS , ou n'importe quoi d'autre.

69
répondu Puppy 2017-11-06 02:06:03
la source

pour les langues ne spécifiant pas de modèle de mémoire, vous écrivez le code pour la langue et le modèle de mémoire spécifié par l'architecture du processeur. Le processeur peut choisir de Ré-ordonner les accès mémoire pour la performance. Ainsi, si votre programme a des courses de données (une course de données est quand il est possible pour plusieurs noyaux / Hyper-threads d'accéder à la même mémoire simultanément) alors votre programme n'est pas plate-forme croisée en raison de sa dépendance sur le processeur modèle de mémoire. Vous pouvez consulter les manuels Logiciels Intel ou AMD pour savoir comment les processeurs peuvent ré-ordonner les accès mémoire.

fait très important, les serrures (et la sémantique de la concurrence avec verrouillage) sont généralement mises en œuvre sur une plate-forme transversale... Donc, si vous utilisez des serrures standard dans un programme multithreaded sans courses de données, alors vous n'avez pas à vous soucier des modèles de mémoire de plateforme .

fait intéressant, Les compilateurs Microsoft pour C++ ont acquis / release sémantique pour volatile qui est une extension C++ pour faire face à l'absence d'un modèle de mémoire en C++ http://msdn.microsoft.com/en-us/library/12a04hfd (v = 80).aspx . Cependant, étant donné que Windows ne fonctionne que sur x86 / x64, cela ne veut pas dire grand chose (les modèles de mémoire Intel et AMD rendent facile et efficace l'implémentation de la sémantique d'acquisition / publication dans un langage).

50
répondu ritesh 2017-11-06 02:09:19
la source

si vous utilisez des Mutex pour protéger toutes vos données, vous ne devriez vraiment pas avoir à vous inquiéter. Les Mutex ont toujours fourni des garanties suffisantes de commande et de visibilité.

maintenant, si vous avez utilisé atomics, ou des algorithmes sans verrouillage, vous devez penser au modèle de mémoire. Le modèle de mémoire décrit précisément quand les atomics fournissent des garanties d'ordre et de visibilité, et fournit des clôtures portatives pour des garanties codées à la main.

auparavant, atomics serait fait en utilisant le compilateur intrinsics, ou une bibliothèque de niveau supérieur. Les clôtures auraient été faites en utilisant des instructions spécifiques au CPU (barrières mémoire).

23
répondu ninjalj 2011-06-12 03:49:53
la source