L'explication de l'ordre détendu est-elle erronée dans la référence?

13

Dans la documentation de std::memory_ordersur cppreference.com, il y a un exemple de commande détendue:

Commande détendue

Les opérations atomiques marquées memory_order_relaxedne sont pas des opérations de synchronisation; ils n'imposent pas d'ordre entre les accès simultanés à la mémoire. Ils garantissent uniquement la cohérence de l'ordre d'atomicité et de modification.

Par exemple, avec x et y initialement zéro,

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

est autorisé à produire r1 == r2 == 42 car, bien que A soit séquencé avant B dans le thread 1 et que C soit séquencé avant D dans le thread 2, rien n'empêche D d'apparaître avant A dans l'ordre de modification de y, et B de apparaissant avant C dans l'ordre de modification de x. L'effet secondaire de D sur y pourrait être visible pour la charge A dans le fil 1 tandis que l'effet secondaire de B sur x pourrait être visible pour la charge C dans le fil 2. En particulier, cela peut se produire si D est terminé avant C dans thread 2, soit en raison de la réorganisation du compilateur, soit au moment de l'exécution.

il dit "C est séquencé avant D dans le thread 2".

Selon la définition de séquencé avant, qui peut être trouvée dans l' ordre d'évaluation , si A est séquencé avant B, alors l'évaluation de A sera terminée avant que l'évaluation de B ne commence. Puisque C est séquencé avant D dans le thread 2, C doit être terminé avant le début de D, par conséquent la partie condition de la dernière phrase de l'instantané ne sera jamais satisfaite.

abigaile
la source
Votre question concerne-t-elle spécifiquement C ++ 11?
curiousguy
non, cela s'applique également à c ++ 14,17. Je sais que le compilateur et le processeur peuvent réorganiser C avec D.Mais si la réorganisation se produit, C ne peut pas être terminé avant que D ne commence. Je pense donc qu'il y a une mauvaise utilisation de la terminologie dans la phrase "A est séquencé avant B dans le thread 1 et C est séquencé avant D dans le thread 2". Il est plus précis de dire "Dans le code, A est PLACÉ AVANT B dans le thread 1 et C est PLACÉ AVANT D dans le thread 2". Le but de cette question est de confirmer cette pensée
abigaile
Rien n'est défini en terme de "réorganisation".
curiousguy

Réponses:

13

Je crois que la référence est juste. Je pense que cela se résume à la règle "comme si" [intro.execution] / 1 . Les compilateurs sont uniquement tenus de reproduire le comportement observable du programme décrit par votre code. Une relation séquentielle avant n'est établie qu'entre les évaluations du point de vue du fil dans lequel ces évaluations sont effectuées [intro.execution] / 15 . Cela signifie que lorsque deux évaluations séquencées l'une après l'autre apparaissent quelque part dans un thread, le code qui s'exécute réellement dans ce thread doit se comporter comme si tout ce que faisait la première évaluation affectait effectivement tout ce que faisait la deuxième évaluation. Par exemple

int x = 0;
x = 42;
std::cout << x;

doit imprimer 42. Cependant, le compilateur n'a pas réellement à stocker la valeur 42 dans un objet xavant de relire la valeur de cet objet pour l'imprimer. Il peut tout aussi bien se rappeler que la dernière valeur à stocker xétait 42 et ensuite simplement imprimer la valeur 42 directement avant d'effectuer un stockage réel de la valeur 42 dans x. En fait, s'il xs'agit d'une variable locale, il peut tout aussi bien suivre la valeur que cette variable a été affectée en dernier à n'importe quel moment et ne jamais même créer un objet ou stocker réellement la valeur 42. Il n'y a aucun moyen pour le thread de faire la différence. Le comportement sera toujours comme s'il y avait une variable et comme si la valeur 42 était réellement stockée dans un objet x avanten cours de chargement à partir de cet objet. Mais cela ne signifie pas que le code machine généré doit réellement stocker et charger n'importe quoi n'importe où. Tout ce qui est requis, c'est que le comportement observable du code machine généré est indiscernable de ce que serait le comportement si toutes ces choses devaient réellement se produire.

Si nous regardons

r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

alors oui, C est séquencé avant D. Mais vu de ce fil isolément, rien de ce que C fait affecte le résultat de D. Et rien de D ne changerait le résultat de C. La seule façon dont l'un pourrait affecter l'autre serait comme une conséquence indirecte de quelque chose qui se passe dans un autre thread. Cependant, en précisant std::memory_order_relaxed, vous avez explicitement déclaréque l'ordre dans lequel la charge et le magasin sont observés par un autre thread n'est pas pertinent. Puisqu'aucun autre thread ne peut observer la charge et stocker dans un ordre particulier, il n'y a rien d'autre qu'un thread puisse faire pour que C et D s'influencent de manière cohérente. Ainsi, l'ordre dans lequel le chargement et le stockage sont réellement effectués n'est pas pertinent. Ainsi, le compilateur est libre de les réorganiser. Et, comme mentionné dans l'explication sous cet exemple, si le stockage de D est effectué avant la charge de C, alors r1 == r2 == 42 peut en effet se produire…

Michael Kenzel
la source
Donc, essentiellement, la norme stipule que C doit se produire avant D , mais le compilateur pense qu'il ne peut pas être prouvé que C ou D se soit produit ensuite et, en raison de la règle de simulation, les réordonne de toute façon, n'est-ce pas?
Fureeish
4
@Fureeish No. C doit se produire avant D pour autant que le fil sur lequel ils se produisent puisse le dire. L'observation d'un autre contexte peut être incompatible avec ce point de vue.
Déduplicateur
5
@curiousguy Cette affirmation semble similaire à vos autres évangélisations C ++ précédentes .
Courses de légèreté en orbite le
1
@curiousguy Michael en a publié une longue explication ainsi que des liens vers les chapitres pertinents de la norme.
Courses de légèreté en orbite le
2
@curiousguy La norme fait d' une étiquette de dispositions de « la règle que, si » dans une note: « Cette disposition est parfois appelée « comme-si » règle » intro.execution
Caleth
1

Il est parfois possible qu'une action soit ordonnée par rapport à deux autres séquences d'actions, sans impliquer aucun ordre relatif des actions dans ces séquences l'une par rapport à l'autre.

Supposons, par exemple, que l'on ait les trois événements suivants:

  • stocker 1 à p1
  • charger p2 dans temp
  • stocker 2 à p3

et la lecture de p2 est ordonnée indépendamment après l'écriture de p1 et avant l'écriture de p3, mais il n'y a pas d'ordre particulier dans lequel participent à la fois p1 et p3. En fonction de ce qui est fait avec p2, il peut être impossible pour un compilateur de reporter p1 au-delà de p3 tout en réalisant la sémantique requise avec p2. Supposons cependant que le compilateur savait que le code ci-dessus faisait partie d'une séquence plus large:

  • stocker 1 à p2 [séquencé avant le chargement de p2]
  • [faites ce qui précède]
  • stocker 3 dans p1 [séquencé après l'autre magasin vers p1]

Dans ce cas, il pourrait déterminer qu'il pourrait réorganiser le magasin en p1 après le code ci-dessus et le consolider avec le magasin suivant, résultant ainsi en un code qui écrit p3 sans écrire p1 en premier:

  • régler la température sur 1
  • stocker la température en p2
  • stocker 2 à p3
  • stocker 3 à p1

Bien qu'il puisse sembler que les dépendances de données entraîneraient un comportement transitoire de certaines parties des relations de séquençage, un compilateur peut identifier des situations où les dépendances de données apparentes n'existent pas et n'auraient donc pas les effets transitifs attendus.

supercat
la source
1

S'il y a deux instructions, le compilateur générera du code dans un ordre séquentiel afin que le code de la première soit placé avant la seconde. Mais les processeurs disposent en interne de pipelines et sont capables d'exécuter des opérations d'assemblage en parallèle. L'instruction C est une instruction de chargement. Pendant que la mémoire est récupérée, le pipeline traitera les quelques instructions suivantes et étant donné qu'elles ne dépendent pas de l'instruction de chargement, elles pourraient finir par être exécutées avant la fin de C (par exemple, les données pour D étaient dans le cache, C dans la mémoire principale).

Si l'utilisateur a vraiment besoin que les deux instructions soient exécutées séquentiellement, des opérations de classement de mémoire plus strictes peuvent être utilisées. En général, les utilisateurs s'en moquent tant que le programme est logiquement correct.

edwinc
la source
-10

Tout ce que vous pensez est également valable. La norme ne dit pas ce qui s'exécute séquentiellement, ce qui ne fonctionne pas et comment cela peut être mélangé .

C'est à vous, et à chaque programmeur, de créer une sémantique cohérente en plus de ce gâchis, un travail digne de plusieurs doctorats.

curiousguy
la source