34
votes

L'ordre des écritures est-il pour séparer les membres d'une structure volatile garantie d'être conservée?

Supposons que j'ai une structure comme ceci:

data.foo = 1;
data.foo = 3;
data.bar = 2;
data.bar = 4;

Les affectations sont-elles toutes garanties à ne pas être réorganisées?

Par exemple sans volatile, le compilateur serait clairement autorisé Pour l'optimiser comme deux instructions dans un ordre différent comme celle-ci:

data.bar = 4;
data.foo = 3;

mais avec volatile, le compilateur est-il nécessaire pour ne pas faire quelque chose comme ça?

volatile struct { int foo; int bar; } data;
data.foo = 1;
data.bar = 2;
data.foo = 3;
data.bar = 4;

(traitant les membres comme des entités volatiles non apparentées distinctes - et effectuant une réorganisation que je peux imaginer qu'il pourrait essayer d'améliorer la localité de référence au cas où foo et bar sont à la limite d'une page - par exemple.)

De plus, la réponse est-elle cohérente pour les versions actuelles des normes C et C ++?


8 commentaires

Je ne sais pas, mais j'espère bien que oui, sinon les structures de file d'attente que j'utilise pour les communications d'interruption peuvent être en difficulté :)


Non réorganisé de citation complète ici pour C ++ (C peut être différent) - en.cppreference.com/ w / cpp / langue / cv "Un objet dont le type est volatil-qualifié, ou un sous-objet d'un objet volatile" ... _ "Chaque opération d'accès (lire ou écrire, appel de fonction membre, etc. .) Fabriqué par une expression glvalue de type qualifié volatil est traité comme un effet secondaire visible aux fins d'optimisation "


S'il s'agit de C ++ et "concurrence" en soi (comme le dit la balise), consultez std :: atomic . Il a des garanties de non-ordinateur similaires.


@Bloody: Malheureusement les types volatiles std :: atomic ont un comportement contre-intuitif, et au moins sur les compilateurs actuels. Par exemple, ici Une charge d'un volatile std :: atomic est optimisé car sa valeur n'est pas utilisée, même si elle ne serait pas pour un volatile ordinaire int .


@NateEldredge Je n'ai jamais pensé à rejoindre std :: atomic avec volatile . Si OP expose cette structure pour l'interaction IO, l'utilisation de volatile est incontestable. Cependant, la balise d'OP suggère qu'il s'agit de concurrence (programme multithread), auquel cas std :: atomic est le bon outil à utiliser et non volatile . C'est peut-être juste un style lâche de dénomination des tags.


@Bloody principalement je regarde C, mais comme il y a souvent des différences subtiles entre les langues (C ++ semble s'être éloigné depuis longtemps de l'objectif d'être un superset), je suis curieux de volatile en particulier car il s'appliquerait à la portabilité de C Code à C ++. Oui, C ++ a en effet de bien meilleures bibliothèques pour gérer ce genre de chose.


@Nateeldredge qui est nécessaire, cela a à voir avec les expressions de valeur jetée et ce qui constitue des lectures. D'un autre côté, vous ne devriez pas volatile std :: atomic en premier lieu de toute façon.


Le compilateur n'est pas obligé de faire quoi que ce soit, ce qui constitue un accès volatil est défini par l'implémentation, la norme définit simplement une certaine relation de commande sur les accès en termes de comportement observable et de la machine abstraite, pour que la documentation de mise en œuvre se réfère. La génération de code n'est pas traitée par la norme.


2 Réponses :


30
votes

Ils ne seront pas réorganisés.

C17 6.5.2.3 (3) dit:

Une expression postfixe suivie du. L'opérateur et un identifiant désignent un membre d'une structure ou objet syndical. La valeur est celle du membre nommé, 97) et est un lvalue si la première expression est une lvalue. Si la première expression a un type qualifié, le résultat a la version so-qualifiée du type du membre désigné.

Puisque data a le type volatile -qualified, alors faites data.bar et data.foo . Ainsi, vous effectuez deux affectations aux objets volatiles int . Et par 6.7.3 Note 136,

Les actions sur les objets ainsi déclarées [comme volatile ] ne seront pas «optimisées» par une mise en œuvre ou réorganisée sauf selon les règles d'évaluation des expressions.

Une question plus subtile est de savoir si le compilateur pourrait les attribuer tous les deux avec une seule instruction, par exemple, s'ils sont des valeurs contiguës de 32 bits, pourrait-il utiliser un magasin 64 bits pour définir les deux? Je ne pense pas, et au moins GCC et Clang n'essayent pas.


11 commentaires

Merci d'avoir cité la norme (comme je n'ai pas de copie), cela semble répondre à la question, mais votre texte "vous attribuez deux objets int volatils" est trompeur en ce que s'ils n'étaient pas considérés comme le même objet, la réponse serait être différent, ou il faudrait une restriction supplémentaire sur le compilateur pour préserver l'ordre des accès volatils même s'ils sont dans des objets non apparentés. Peut-être mieux pour garder la citation et affiner le texte de réponse ...


Je pense que la modification des opérations pour être simultanée (en utilisant une instruction pour deux affectations) devrait compter comme une réorganisation. Si ce n'est pas par une interprétation stricte de la norme, alors certainement par l'esprit de la norme, la raison d'une telle restriction (qui a une pénalité de performance) s'applique, que vous obteniez ou non le libellé.


@TedshaneyFelt: Reformué sur "deux affectations à volatiles int objets".


Vous voulez dire deux affectations à l'objet int volatile? Ce serait satisfaisant.


Notez que ce sont des parties différentes du même objet, pas des objets volatils séparés, mais le même objet, que la première citation de la spécification que vous avez donnée a souligné ...


@Ben doit être correct sur le fait que la réorganisation simultanée. La modification des opérations pour être simultanée affecterait le matériel, par exemple, la définition de bits de données, puis basculer un bit stroboscopique sur les E / S mappées de mémoire est clairement quelque chose qui serait optimisé si cela était autorisé.


Il est défini par l'implémentation ce qui constitue l'accès à un objet qualifié volatil. Si la mise en œuvre C cible le matériel sur lequel les effets d'une écriture 64 bits pourraient être les mêmes que deux écritures 32 bits (par exemple, deux écritures 32 bits peuvent être vues séparément par d'autres composants partageant de la mémoire, mais ils pourraient être considérés comme indiscernable, donc une écriture 64 bits qui est nécessairement simultanée est indiscernable de deux écritures 32 bits qui se trouvent efficacement simultanées), alors il pourrait être raisonnable que la mise en œuvre définisse «l'accès» afin qu'une écriture 64 bits puisse être utilisé.


@Eric postpischil Pour que ce soit le cas, ils ne seraient pas vraiment écrits simultanément, même s'ils sont optimisés en une seule instruction. Ensuite, cela semble ok. Mais s'ils se distinguent, ce qu'ils seraient si le stroboscopte devenait actif au fur et à mesure que les données étaient écrites au lieu de la suite, alors elle serait mal réorganisée pour être simultanée. Le compilateur devrait prendre en considération si l'alignement est tel qu'il pourrait s'en tirer avec une seule instruction d'écriture divisée en deux accès d'écriture de données.


@Tedshaneyfelt: Les membres d'un type de structure sont eux-mêmes des objets. 6.2.5 (20): "Un type de structure décrit un ensemble non vide séquentiellement alloué d'objets de membre ". Nous effectuons donc en effet deux accès à des objets volatils, et ils sont des objets différents, mais ils font également partie de l'objet data . J'ai changé le libellé pour indiquer clairement que la réorganisation serait toujours interdite même pour deux accès au même objet (ce qui n'est pas le cas à accomplir).


1. Oui, bien sûr, les membres individuels d'un objet sont eux-mêmes des objets. 2. Oui, la norme de la note de bas de page 136 interdit clairement l'optimisation des accès tels que: data.bar = 4; data.foo = 3; ferait le cas. 3. La note de bas de page pourrait être interprétée comme «des actions sur [l'un des] objets ainsi déclarés [comme volatils] ne seront pas« optimisés »par une mise en œuvre ou réorganisés, sauf comme le permet les règles d'évaluation des expressions. [Mais sa relation avec D'autres objets de ce type ne sont pas pris en compte ici], donc le fait qu'ils font partie du même objet semble pertinent.


En ce qui concerne la préservation de l'ordre des opérations ... open- std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html



17
votes

Si vous souhaitez l'utiliser dans plusieurs threads, il y a un gotcha significatif.

Alors que le compilateur ne réorganisera pas les écritures de variables volatiles (comme décrit dans La réponse de Nate Eldredge ), il y a un autre point où la réorganisation de l'écriture peut se produire, et c'est le processeur lui-même. Cela dépend de l'architecture du CPU, et quelques exemples suivent:

Intel 64

Voir Intel® 64 Architecture Ordroding White White Paper .

Bien que les instructions du magasin ne soient pas réorganisées (2.2):

  1. Les magasins ne sont pas réorganisés avec les autres magasins.
  2. Ils peuvent être visibles à différents CPU dans un ordre différent (2.4):

    La commande de mémoire Intel 64 permet aux magasins de deux processeurs d'être vus dans différents ordres par Ces deux processeurs

    AMD 64

    AMD 64 (qui est le X64 commun) a un comportement similaire dans la spécification :

    Généralement, les écritures hors service ne sont pas autorisées. Les instructions d'écriture exécutées hors de l'ordre ne peuvent pas commettre (écrire) leur résultat à la mémoire jusqu'à ce que toutes les instructions précédentes soient terminées dans l'ordre du programme. Le processeur peut cependant maintenir le résultat d'une instruction d'écriture hors service dans un tampon privé (non visible par le logiciel) jusqu'à ce que ce résultat puisse être engagé dans la mémoire.

    powerpc

    Je me souviens avoir dû faire attention à cela sur xbox 360 qui a utilisé un CPU POWERPC :

    Bien que le CPU Xbox 360 ne réorganise pas les instructions, il réorganise les opérations d'écriture, qui se terminent après les instructions elles-mêmes. Ce réarrangement des écritures est spécifiquement autorisé par le modèle de mémoire PowerPC

    Pour éviter de réorganiser le processeur de manière portable, vous devez utiliser mémoire Fences Comme C ++ 11 std :: atomic_thread_fence ou c11 atomic_thread_fence . Sans eux, l'ordre des écritures vues à partir d'un autre fil peut être différent.

    Voir aussi c ++ 11 a introduit un modèle de mémoire standardisé. Qu'est-ce que ça veut dire? Et comment cela va-t-il affecter la programmation C ++?

    Ceci est également noté dans le wikipedia Memory Barrier Article:

    De plus, il n'est pas garanti que les lectures et les écritures volatiles seront vues dans le même ordre par d'autres processeurs ou noyaux dus à la mise en cache, au protocole de cohérence du cache et à l'ordre de mémoire détendue, ce qui signifie que les variables volatiles peuvent même ne pas fonctionner comme interdiction drapeaux ou mutex.


3 commentaires

"Cela soulève la question de savoir si un volatile devrait avoir une signification réelle qui offre à la fois l'atomicité et la visibilité inter-thread, à peu près dans le sens des volatils Java. Bien que nous pensons que, abstraits, cela offre une amélioration substantielle en donnant la sémantique à quelque chose qui a actuellement Presque aucune sémantique portable, il semble y avoir un certain nombre d'obstacles pratiques motivés par des problèmes de compatibilité arriérée qui nous amènent au moins. " - Hans Boehm & Nick Maclaren open-std. org / jtc1 / sc22 / wg21 / docs / papiers / 2006 / n2016.html ...


La préoccupation de Boehm & Maclaren aurait pu être traitée peut-être par le comité des normes ajoutant une construction syntaxique dans laquelle les volatils seraient obligés de se comporter davantage le long de l'esprit de volatilité qu'ils hésitent à exiger pour des raisons de compatibilité en rétrocassemblée. par exemple. S Nouvelle syntaxe: Volatile {Block} serait un ajout suffisant à la langue pour permettre la compatibilité vers l'arrière, mais pour permettre également un comportement significatif et utile plus intuitif des objets volatils dans ce bloc. Comme l'espace de noms, il peut être préférable de lui permettre d'étendre plusieurs définitions de fonctions. Comme c'est maladroit.


Si vous utilisez cela à partir de plusieurs threads, vous avez une course de données et que tous les paris sont désactivés. Contrairement aux types atomic , les objets volatiles ne sont pas sûrs de thread et n'évitent pas les races de données. La seule utilisation viable pour volatile ces jours-ci consiste à accéder aux appareils matériels mappés par mémoire, et dans ce cas, vous aurez normalement la mémoire marquée comme "non achetée" d'une manière spécifique à la machine, ce qui est supposé Pour inhiber la réorganisation du CPU et garantir que l'appareil voit les charges et les magasins dans l'ordre du programme (au niveau de l'assemblage).