Pour {A = a; B = b; }, “A = a” sera-t-il ssortingctement exécuté avant “B = b”?

Supposons que A , B , a et b sont toutes des variables et que les adresses de A , B , a et b sont toutes différentes. Ensuite, pour le code suivant:

 A = a; B = b; 

Les standards C et C ++ requièrent-ils explicitement que A=a soit exécuté ssortingctement avant B=b ? Étant donné que les adresses de A , B , a et b sont toutes différentes, les compilateurs sont-ils autorisés à échanger la séquence d’exécution de deux instructions dans un but comme l’optimisation?

Si la réponse à ma question est différente en C et C ++, j’aimerais connaître les deux.

Edit: Le fond de la question est le suivant. Dans la conception de l’IA du jeu de société, pour l’optimisation, les utilisateurs utilisent une table de hachage partagée sans locking , dont la correction dépend fortement de l’ordre d’exécution si nous n’ajoutons pas de ressortingction volatile .

Les deux normes permettent que ces instructions soient exécutées dans le désordre, dans la mesure où cela ne change pas le comportement observable. Ceci est connu comme la règle as-if:

Notez que, comme le soulignent les commentaires, le terme “comportement observable” désigne le comportement observable d’un programme ayant un comportement défini. Si votre programme a un comportement indéfini, le compilateur est dispensé de raisonner à ce sujet.

Le compilateur est seulement obligé d’émuler le comportement observable d’un programme, donc si un réordonnancement ne violerait pas ce principe, il serait alors autorisé. En supposant que le comportement est bien défini, si votre programme contient un comportement indéfini tel qu’une course de données, le comportement du programme sera imprévisible et, comme indiqué dans les commentaires, il faudra utiliser une forme de synchronisation pour protéger la section critique.

Une référence utile

Un article intéressant qui couvre ceci est la commande de mémoire à la compilation et il dit:

La règle cardinale de la réorganisation de la mémoire, qui est universellement suivie par les développeurs de compilateurs et les fournisseurs de processeurs, pourrait être libellée comme suit:

Tu ne modifieras pas le comportement d’un programme mono-thread.

Un exemple

L’article fournit un programme simple où nous pouvons voir cette réorganisation:

 int A, B; // Note: static storage duration so initialized to zero void foo() { A = B + 1; B = 0; } 

et montre à des niveaux d’optimisation plus élevés B = 0 se fait avant A = B + 1 , et nous pouvons reproduire ce résultat en utilisant godbolt , ce qui en utilisant -O3 produit ce qui suit ( voir en direct ):

 movl $0, B(%rip) #, B addl $1, %eax #, D.1624 

Pourquoi?

Pourquoi le compilateur réorganise-t-il? L’article explique que c’est exactement la même raison pour laquelle le processeur le fait, en raison de la complexité de l’architecture:

Comme je l’ai mentionné au début, le compilateur modifie l’ordre des interactions mémoire pour la même raison que le processeur le fait – l’optimisation des performances. De telles optimisations sont une conséquence directe de la complexité du processeur moderne.

Normes

Dans le projet de norme C ++, cela est traité dans la section 1.9 Exécution du programme qui indique ( accentuation à venir ):

Les descriptions sémantiques de la présente Norme internationale définissent une machine abstraite non déterministe paramétrée. La présente Norme internationale n’impose aucune exigence concernant la structure des implémentations conformes. En particulier, ils n’ont pas besoin de copier ou d’émuler la structure de la machine abstraite. Les implémentations conformes sont plutôt nécessaires pour émuler (uniquement) le comportement observable de la machine abstraite, comme expliqué ci-dessous. 5

la note de bas de page 5 nous dit que cette règle est également connue sous le nom de règle

Cette disposition est parfois appelée règle «comme si» , car une mise en œuvre est libre de ne pas tenir compte de toute exigence de la présente Norme internationale, à condition que le résultat soit conforme à ce qui peut être déterminé par le comportement observable. du programme. Par exemple, une implémentation réelle n’a pas besoin d’évaluer une partie d’une expression si elle peut en déduire que sa valeur n’est pas utilisée et qu’aucun effet secondaire affectant le comportement observable du programme n’est produit.

le projet de norme C99 et le projet de norme C11 couvrent ce point dans la section 5.1.2.3 Exécution du programme, bien que nous devions aller à l’index pour voir qu’il est également appelé la règle du standard C:

comme si règle, 5.1.2.3

Mise à jour sur les considérations de locking

L’article Une introduction à la programmation sans locking couvre bien ce sujet et pour les préoccupations des OP sur l’implémentation des tables de hachage partagées sans locking, cette section est probablement la plus pertinente:

Commande de mémoire

Comme le suggère l’organigramme, chaque fois que vous effectuez une programmation sans locking pour le multicœur (ou tout multiprocesseur symésortingque ) et que votre environnement ne garantit pas la cohérence séquentielle, vous devez déterminer comment empêcher la réorganisation de la mémoire .

Sur les architectures actuelles, les outils pour appliquer un ordre de mémoire correct se divisent généralement en trois catégories, ce qui empêche à la fois la réorganisation du compilateur et la réorganisation du processeur :

  • Une instruction de synchronisation ou de clôture légère, dont je parlerai dans les prochains articles ;
  • Une instruction de clôture de mémoire complète, que j’ai démontrée précédemment ;
  • Les opérations de mémoire qui fournissent la sémantique d’acquisition ou de libération.

L’acquisition de la sémantique empêche la réorganisation de la mémoire des opérations qui la suivent dans l’ordre des programmes, et la sémantique de la libération empêche la réorganisation de la mémoire des opérations qui la précèdent. Ces sémantiques sont particulièrement appropriées dans les cas où il existe une relation producteur / consommateur, où un thread publie des informations et l’autre le lit. Je vais aussi en parler davantage dans un prochain post.

S’il n’y a pas de dépendance des instructions, celles-ci peuvent être exécutées dans le désordre même si le résultat final n’est pas affecté. Vous pouvez observer ceci lors du débogage d’un code compilé au niveau d’optimisation supérieur.

Depuis A = a; et B = b; sont indépendants en termes de dépendances de données, cela ne devrait pas avoir d’importance. S’il y a eu un résultat / résultat de l’instruction précédente affectant l’entrée de l’instruction suivante, alors l’ordre est important, sinon non. c’est une exécution ssortingctement séquentielle normalement.

Ma lecture est que cela est nécessaire pour fonctionner avec le standard C ++; Cependant, si vous essayez de l’utiliser pour le contrôle multithreading, cela ne fonctionne pas dans ce contexte car rien ne garantit que les registres seront écrits en mémoire dans le bon ordre.

Comme votre édition l’indique, vous essayez de l’utiliser exactement là où cela ne fonctionnera pas.

Il peut être intéressant que si vous faites ceci:

 { A=a, B=b; /*etc*/ } 

Notez la virgule à la place du point-virgule.

Ensuite, la spécification C ++ et tout compilateur de confirmation devront garantir l’ordre d’exécution car les opérandes de l’opérateur de virgule sont toujours évalués de gauche à droite. Cela peut en effet être utilisé pour empêcher l’optimiseur de perturber la synchronisation de votre thread en le réordonnant. La virgule devient effectivement une barrière à travers laquelle le réordonnancement n’est pas autorisé.