64
votes

Un compilateur C est-il autorisé à fusionner les affectations séquentielles aux variables volatiles?

J'ai un problème matériel théorique (non déterministe, difficile à tester, jamais arrivé en pratique) signalé par le fournisseur matériel où l'écriture à deux mots dans certaines gammes de mémoire peut corrompre les futurs transferts de bus.

Alors que je Je n'ai pas d'écriture à double mot explicitement dans le code C, je crains que le compilateur soit autorisé (dans les implémentations actuelles ou futures) pour fusionner plusieurs affectations de mots adjacentes en une seule affectation à double mot.

Le compilateur n'est pas autorisé à réorganiser les affectations de volatiles, mais il n'est pas clair (pour moi) si la coalescence compte comme réorganisation. Mon intestin dit que c'est le cas, mais j'ai déjà été corrigé par les avocats du langage!

Exemple:

xxx

(Je vais demander à mon fournisseur de compilateur sur Ceci séparément, mais je suis curieux de savoir ce qu'est l'interprétation canonique / communautaire de la norme.)


7 commentaires

stackoverflow.com/questions/52186834/…


Avez-vous vérifié l'assemblage généré par le compilateur pour voir s'il fait cela?


Si la mémoire est cartographiée sous forme de "mise en cache" ou de "rédaction d'écriture", il pourrait s'agir du MMU combinant les deux écritures à mot unique en écriture à double mot.


@EricPostPischil y travaillant. Faire des scripts pour filtrer les occurrences possibles. Le système de construction de projet résiste :-(


@Lundin qui ressemble maintenant à l'API du vendeur.


@Ianabbott Hardware Vendor Report indique explicitement une liste d'instructions. Pourrait être pertinent pour quelqu'un d'autre. Bon point.


@EricPostPischil vérifié. Le compilateur ne fusionne pas de telles écritures. Bien que je n'ai pas envisagé -LTO ... La chaîne d'outils et le système de construction du projet ne sont pas très favorables à l'obtention de l'assembleur après le lien, donc je ne vais pas tester plus loin. Fera confiance aux canaux de soutien au consensus et aux fournisseurs communautaires que cela ne se produira pas.


5 Réponses :


50
votes

Non, le compilateur n'est absolument pas autorisé à optimiser ces deux écritures en une seule écriture à double mot. Il est un peu difficile de citer la norme car la pièce concernant les optimisations et les effets secondaires est si floue. Les pièces pertinentes se trouvent dans C17 5.1.2.3:

Les descriptions sémantiques de cette norme internationale décrivent le comportement d'un Machine abstraite dans laquelle les problèmes d'optimisation ne sont pas pertinents.

accéder à un objet volatil, modification d'un objet, modification d'un fichier ou appeler une fonction qui fait l'une de ces opérations sont tous des effets secondaires, qui sont des changements dans l'état de l'environnement d'exécution.

Dans la machine abstraite, toutes les expressions sont évaluées comme spécifié par la sémantique. Une implémentation réelle n'a pas besoin d'évaluer une partie d'une expression si elle peut déduire que sa valeur n'est pas utilisée et qu'aucun effet secondaire nécessaire n'est produit (y compris toute cause en appelant une fonction ou en accédant à un objet volatil).

Les accès aux objets volatils sont évalués strictement selon les règles de la machine abstraite.

Lorsque vous accédez à une partie d'une structure, qui en soi est un effet secondaire, qui peut avoir des conséquences que le compilateur ne peut pas déterminer. Supposons par exemple que votre structure est une carte de registre matériel et que ces registres doivent être écrits dans un certain ordre. Comme par exemple, une documentation de microcontrôleur pourrait être dans le sens de: "Reg0 permet le périphérique matériel et doit être écrit avant de pouvoir configurer les détails de Reg1".

Un compilateur qui fusionnerait l'objet volatile Écrit en un seul serait non conforme et brisé.


3 commentaires

Ohhh ne pensait pas à l'accès à la structure. Le pointeur dans ce cas ne doit pas être volatile alors, ne laissant que les membres volatils (et dans le trou de lapin volatil imbriqué que nous allons). Merde, C est difficile. Heureux de voir que vous avez pu regarder au-delà de cela. Le code "réel" en question n'a pas cet aspect, mais il était trop noueux pour faire un bon exemple.


@Andreas Si l'accès à la structure est volatile, l'accès aux membres sera volatile même si les membres ne sont pas déclarés volatils. Identique à "const".


C'est faux, bien qu'une mauvaise conception très courante. Les cartes standard du programme des textes aux séquences d'actions observables de la machine abstraite. Cela ne dit pas comment ceux-ci se reflètent dans la réalité. De plus, il dit explicitement que ce qui constitue un accès volatil, ce qui devient une action volatile observée à l'extérieur, est définie par l'implémentation. La norme ne dit rien sur le code d'objet.



6
votes

Le modification changera le comportement observable du programme. Le compilateur n'est donc pas autorisé à le faire.


1 commentaires

La séquence des opérations de mémoire matérielle réelles n'est "observable" que si une implémentation choisit de le spécifier en tant que telle. Rien n'interdise une implémentation à partir de sa propre machine virtuelle où les magasins volatils mettent immédiatement à jour l'état de la machine virtuelle, mais de telles mises à jour mettent un certain temps à être traduites en opérations sur le matériel de la machine réel.



31
votes

Le compilateur est pas autorisé à faire de deux ces affectations en une seule écriture de mémoire. Il doit y avoir deux écritures indépendantes à partir du noyau. La réponse de @lundin donne des références pertinentes à la norme C.

Cependant, sachez qu'un cache - s'il est présent - peut vous tromper. Le mot-clé volatile n'implique pas la mémoire "non achetée". Ainsi, en plus d'utiliser volatile , vous devez également vous assurer que l'adresse 0xff000000 est mappée comme non achetée. Si l'adresse est mappée comme mise en cache, le cache HW peut transformer les deux affectations en une seule écriture de mémoire. En d'autres termes - pour la mémoire en cache, deux opérations d'écriture de mémoire de base peuvent se retrouver comme une seule opération d'écriture sur l'interface de mémoire des systèmes.


29 commentaires

Volatile signifie absolument une mémoire non achetée. Un système qui préfère les lectures de Volatile Les variables qualifiées n'est pas conforme. L'accès volatile doit être effectué en fonction des points de séquence placés autour des variables. Au fur et à mesure que les CPU ont évolué, il y a eu des tentatives par les fournisseurs de matériel et / ou de compilateur pour pousser ce fardeau de comportement de forme de mémoire sur les programmeurs d'application. Mais C n'a jamais permis d'exécution spéculative ou hors de l'ordre de l'accès volatile . Ce n'est pas la faute du programmeur d'application si quelqu'un a publié du matériel qui ne peut pas exécuter conforme C.


@Lundin J'aime voir une référence pour cette affirmation car je ne suis pas d'accord. De plus, ce petit exemple ideone.com/u8sq9n montre que le compilateur ne mappe pas les variables volatiles toutes les variables ordinaires .


Arguments et références ici: stackoverflow.com/a/58697222/584518 . De toute évidence, vous ne pouvez pas utiliser une sortie du compilateur comme preuve de quoi que ce soit, car le problème est que les compilateurs ne se soucient pas de mettre en œuvre volatile en tant que barrière de mémoire, car ils ne peuvent que faire beaucoup sur le matériel sous-jacent.


J'ai vu quelque chose dans la fiche technique où plusieurs mots à double mot initie un transfert d'éclatement / bloc plus grand (mot multi-double?) Sur le bus. Ce serait un aspect hors de portée, car le rapport du fournisseur HW indique que "ces instructions ne fonctionneront parfois pas pour écrire à ces adresses". Ai-je bien compris ou vouliez-vous dire autre chose?


Les implémentations @Andreas HW utilisent de nombreuses optimisations que nous ne voyons pas. Une telle optimisation consiste à faire à la fois des lectures et des écritures dans des troncs plus grands que celles demandées par le noyau du processeur. Cela arrive tout le temps. Pour les données cachables, ce n'est pas un problème. En tant que programmeurs, nous ne nous en soucions normalement pas et nous n'en avons pas besoin. L'exception est lorsque nous écrivons à (registres) dans d'autres appareils HW. Ici, nous devons être sûrs que toutes les lectures et écritures se produisent dans l'ordre et avec la taille exacte que notre code dit. Une façon de garantir cela consiste à s'assurer que l'espace d'adressage de ce dispositif externe est cartographié comme non acheté.


@Lundin donc votre point est que (de nombreux) systèmes / compilateurs modernes violent la norme C car ils ne garantissent pas qu'une variable définie à l'aide de volatile sera réellement écrite en mémoire - est-ce votre point?


@Lundin re: "De toute évidence, vous ne pouvez pas utiliser une sortie du compilateur comme preuve de quoi que ce soit .." Non, et ce n'était pas l'intention. L'intention était juste de montrer un exemple d'un compilateur / système qui n'a pas car carte des variables volatiles différentes des autres variables.


@ 4386427 Ils violent la norme C car le point où la variable volatile est accessible n'est pas l'endroit où "la sémantique" le spécifie, sous la forme de points de séquence. L'accès peut se produire entre deux points de séquence mais pas à l'extérieur d'eux. Et naturellement, cela compte beaucoup en cas de registres matériels ou de tampons DMA, etc. Si un compilateur / système va des bananes et pré-fouetter ces variables en cache de données, alors elle n'est pas seulement non conforme, elle est également cassée et inutile.


@Lundin hmm ... Je ne pense pas que je veux me présenter à une discussion pour savoir si l'industrie entière construit des systèmes qui ne sont pas conformes à la norme. Je ne suis pas sûr d'avoir le temps. Pouvons-nous au moins convenir que a) les compilateurs ne sont pas autorisés à optimiser deux écritures à des variables volatiles en une seule écriture et b) que les compilateurs traditionnels ne garantissent pas que les variables volatiles sont mappées à des zones de mémoire non achetées?


a) Oui b) dépend de ce que vous voulez dire avec le courant dominant. x86, alors ouais tous les paris sont probablement désactivés. Bras ou powerpc ... alors cela dépend du noyau et du compilateur.


@Lundin d'accord, je m'installe avec ça. J'utilise pas mal de bras et de powerPC dans des systèmes intégrés. Je ne me souviens pas d'un compilateur qui a fait quelque chose de spécial pour les volatiles, mais là encore - je n'ai pas essayé tous les cœurs / compilateurs, donc je ne vais pas prétendre qu'il n'a jamais été géré par le compilateur.


@Andreas pour répondre à votre préoccupation différemment. Si vous utilisez les gammes "défectueuses" des vendeurs uniquement comme des zones non cachetées et que vous déclarez vos variables comme volatiles, vous devriez être en sécurité tant que votre code ne fait pas explicite une double écriture. Rien dans le système ne changera les écritures simples pour doubler. Et .. Si les gammes sont utilisées pour les appareils HW mappés de mémoire (comme votre question le suggère), vous devrez de toute façon faire les deux premières choses.


@Lundin: C n'a jamais permis d'exécution spéculative ou OOO de l'accès volatil - c'est différent de "non-procureté". Vous semblez parler de ne pas hisser des charges / des magasins de naufrage à partir de boucles dans ASM. Mais c'est totalement différent de matériel préfectez sur les régions de mémoire cache-back cacheables. Vous pouvez le considérer comme C garantissant que les charges / magasins dans le domaine de cohérence du cache sont un effet secondaire visible, pas le vrai contenu de DRAM. SW ne peut pas observer DRAM (sauf peut-être via une autre cartographie de la même adresse physique, ou sur un système hypothétique avec une mémoire partagée non cohérente)


@Lundin: Si vous souhaitez que les accès MMIO fonctionnent correctement, vous devez vous assurer que la plage d'adresses, y compris l'adresse MMIO, est mappée sans compensation même si vous écrivez ASM à la main; Il est invraisemblable et peu pratique pour un compilateur C de le faire pour vous pour Global volatile int foo; .


@PeterCordes Mais c'est toujours tout le point: ils ont rendu du matériel incompatible avec la langue C. Il n'y a pas beaucoup de compilateurs à faire à ce sujet. Bien que je suppose que les chaînes d'outils pourraient en théorie créer des segments de mémoire inaccessibles et allouer des variables volatiles . Bien que dans le cas des registres, ils doivent être à des adresses très spécifiques, naturellement, une extension non standard est requise. Ici C aurait pu avoir standardisé quelque chose, comme l'opérateur d'extension standard couramment utilisé @ 0x1234 pour allouer quelque chose à une adresse spécifique.


@Lundin: Vous semblez avoir décidé que DRAM lui-même, pas la vue cohérente de la mémoire cohérente au cache que tous les noyaux partagent, est ce que la norme C signifie par "l'environnement d'exécution". Oui, votre argument découlerait de cette prémisse. Mais je ne vois pas une bonne raison de choisir cela, et cela a très peu de sens pour moi dans une implémentation C pour un système avec un cache cohérent. Le contournement du cache rendrait une exagération inutilisante inutilisablement lente pour beaucoup de choses, et inciterait les utilisateurs à rechercher un mécanisme qui n'était pas horrible. par exemple. Pour des trucs comme Volatile SIG_ATOMIC_T , pour s'assurer que les magasins de fichiers mmapés se produisent.


@Lundin: l'accord actuel de facto sur ce que est volatile est très utile, et en fait pendant des années (avant C11), et toujours dans un code, a été utilisé avec succès pour la communication inter-thread, même Avant que la langue n'avait un modèle de mémoire formelle. (Grâce à plus de comportement standard de facto dans ce cas). Les cas d'utilisation pour vouloir un volatile qui contournait vraiment le cache est extrêmement petit. Je peux voir un peu de mérite pour volatile Imply séquentiel cohérence de tous les accès volatils, bloquant le runtime réorganisant d'accès (au moins le cache).


@PeterCordes c'est parce que le timing est important. Lorsque vous déclarez quelque chose de volatil, vous voulez qu'il soit accessible au moment où le code accédant à la variable est exécuté. En théorie, je pourrais déclarer une variable volatile et supposer qu'une récupération lente de la RAM se produira dans le cadre de mon calcul de synchronisation. Pas un problème sur x86 peut-être car ils sont rarement utilisés pour les systèmes en temps réel. Mais dans les systèmes en temps réel intégrés, le timing pourrait beaucoup avoir d'importance.


@Lundin: (suite à partir des commentaires précédents.) Dans ISO C11, Volatile ne contourne / évite pas les données de données UB comme le fait _atomic , bien que l'on puisse affirmer que c'est uniquement parce que ISO C11 ne nécessitent des caches cohérentes, sauf pour les performances de libération / acquisition. Mais à moins que vous ne vouliez affirmer que le dés-facto volatile est thread-safe avec la sémantique qu'il a, ISO C11 a choisi pas pour donner volatile et sémantique inter-thread.


@Lundin: Linkers et Software pour contrôler les attributs de type mémoire comme rendre une gamme inaccessible, donnez-vous les outils pour configurer une mémoire inaccessible à partir de si c'est ce que vous voulez, lors de la programmation d'un système qui a du cache. Je n'achète pas du tout cet argument de timing. Si vous voulez quelque chose de plus lent pour un retard, faites une lecture volatile de la mémoire inaccessible , pas seulement à partir d'une variable arbitraire. Avoir chaque volatile est nécessairement lent est un design pire que je ne voudrais pas.


@Lundin Vous pouvez qualifier des variables automatiques en tant que volatile . Cela signifie-t-il alors que le compilateur doit émettre du code pour désactiver la mise en cache pour cette section de la pile? Je n'ai jamais vu cela auparavant et semble absurde. (Une variable automatique qualifiée volatile est très utile, par exemple si vous passez une seule étape via le programme et que vous souhaitez le changer à partir d'un débogueur).


@PeterCordes: à droite, personne ne "dirait que volatile de-facto est file Vu dans cet ordre par tous les autres threads / cœurs) Volatile n'est pas suffisant pour garantir le code de filetage. Pour ceux qui s'intéressent aux détails sanglants divertissants sur le fonctionnement de la cohérence du cache (ou ne fonctionnent pas sans obstacles appropriés), voir le livre disponible librement de Paul McKenney arXiv.org/abs/1701.00854 , Annexe C, sections C.3.2 Stravorisation du magasin et C.3.3 Tampons de magasin et barrières de mémoire


@PeterCordes: Tout dans la norme C, interdire une implémentation par exemple. Traitement des lectures volatiles et écrit comme des appels à une fonction qui a été documentée comme par exemple En utilisant des rouges matériels normaux et des écritures pour toutes les adresses autres que 0x12340000 et 0x12340001, mais verrouillerait les écritures de byte à 0x12340000 sans les transmettre au matériel, et convertir les écritures d'octets de 0x12340001 en écritures de mots qui feraient le dernier code de valeur à 0x12340000?


@AMDN: Lorsque volatile a été ajouté à la langue, je pense que cela devait servir de "fourre-tout" avec la sémantique la plus serrée dont une application pourrait avoir besoin. Les implémentations où cela représentent un fardeau de performance inacceptable pourrait étendre la langue avec des moyens de demander une sémantique plus faible, mais je pense que le mot-clé était destiné à permettre aux programmes d'assurer un comportement correct - même si ce n'est une performance optimale - sans l'utilisation des extensions spécifiques au compilateur .


@FUZ: Je ne m'attendrais pas à ce qu'un compilateur désactive la mise en cache, mais une implémentation destinée à la programmation de bas niveau devrait permettre à un programmeur qui est capable de le faire pour exploiter la sémantique résultante.


(Corriger un commentaire antérieur: j'ai suggéré qu'une autre cartographie de la même page physique pourrait voir un dram Tout d'abord. Je ne pense pas que la plupart des ISA autorisent une autre cartographie pour contourner la cohérence du cache et lire (ou écrire) DRAM tandis que ce noyau ou un autre a une copie sale d'une ligne (ou de tout rapport pour écrire). Certains ISA peuvent ne pas avoir de DMA cohérent en cache, bien que X86 ait toujours depuis les premiers X86 avec des caches, pour une compat arrière avec les Os existantes comme toujours)


@supercat: Si un type est plus large que char , une meilleure conception serait d'appeler simplement une aide pour les réserves volatiles de ces types si elle devait se combiner. Si les magasins de charbon volatils auraient une signification physique (mais "mal" pour conduire ce matériel), et volatile short ou volatile int sont également possibles pour au moins cette adresse, Ensuite, je pense que le libellé de volatile nécessiterait une implémentation pour permettre aux applications de se tirer dans le pied. Mais s'il n'y a pas d'autre moyen, oui, il pourrait être justifiable de jouer rapidement et à perdre avec le sens de "l'environnement d'exécution".


@PeterCordes: Une implémentation qui effectue une telle virtualisation peut être en mesure de prendre du code qui a été écrit pour une plate-forme matérielle et générer du code machine qui fonctionnera sur un autre, tout en permettant à la plupart des parties non spécifiques au code non machine du code à pleine vitesse. Les opérations d'E / S virtualisées seraient évidemment beaucoup plus lentes sur la nouvelle plate-forme que les E / S directes ne l'auraient été, mais si la plupart du temps d'un programme serait consacré à des choses autres que l'E / S, cela pourrait ne pas avoir trop d'importance.


@PetercOrdes: Soit dit en passant, il peut être utile de noter que toute lecture ou écriture d'un registre matériel qui ne stocke pas la valeur écrite de manière à être un "objet" invoque un comportement indéfini, si un qualificatif volatil est ou non spécifié. Les compilateurs destinés à la programmation de bas niveau ne devraient pas l'utiliser comme une excuse pour se comporter inutilement, mais peuvent l'utiliser pour justifier tout écart par rapport au comportement normal qui pourrait être utile.



9
votes

La norme C est agnostique à toute relation entre les opérations sur des objets volatils et des opérations sur la machine réelle. Alors que la plupart des implémentations spécifieraient qu'une construction comme * (char volatile *) 0x1234 = 0x56; générerait une boutique d'octets avec une valeur 0x56 à l'adresse matérielle 0x1234, une implémentation pourrait, à son sujet, allouer un espace pour par exemple. Un tableau 8192-octet et spécifiez que * (char volatile *) 0x1234 = 0x56; stockerait immédiatement 0x56 à l'élément 0x1234 de ce tableau, sans jamais rien faire avec l'adresse matérielle 0x1234. Alternativement, une implémentation peut inclure un processus qui stocke périodiquement quoi qu'il en soit dans 0x1234 de ce tableau à l'adresse matérielle 0x56.

Tout ce qui est requis pour la conformité, c'est que toutes les opérations sur des objets volatils dans un seul thread sont, du point de vue de la machine abstraite , considérée comme absolument séquencée. Du point de vue de la norme, les implémentations peuvent convertir ces accès en opérations de machine réelles de la manière dont ils jugent bon.


2 commentaires

De plus, ce qui constitue un accès volatil est défini par l'implémentation.


@Philipxy: En effet. Les compilateurs commerciaux traiteraient généralement une écriture volatile comme forçant un compilateur à rincer efficacement tous les objets «enregistrés en cache», permettant de code pour des choses comme les E / S en arrière-plan qui ont été écrites pour un tel compilateur sur une plate-forme particulière pour travailler avec tout autre compilateur de fournisseur qui compilateur qui compila le compment utilisé une sémantique similaire. Clang et GCC, cependant, refusent de soutenir une telle sémantique car ils voient un code tel que "cassé".



15
votes

Le comportement de volatile semble être à la hauteur de l'implémentation, en partie à cause d'une phrase curieuse qui dit: "Ce qui constitue un accès à un objet qui a un type qualifié volatil est défini de l'implémentation" .

Dans ISO C 99, section 5.1.2.3, il y a aussi:

3 Dans la machine abstraite, toutes les expressions sont évaluées comme spécifié par la sémantique. Un La mise en œuvre réelle n'a pas besoin d'évaluer une partie d'une expression si elle peut déduire que son la valeur n'est pas utilisée et qu'aucun effet secondaire nécessaire n'est produit (y compris toute causée par Appeler une fonction ou accéder à un objet volatil ).

Ainsi, bien que les exigences soient données qu'un objet volatile doit être traité conformément à la sémantique abstraite (c'est-à-dire non optimisée), curieusement, La sémantique abstraite elle-même permet le le Per permet Élimination du code mort et des flux de données, qui sont des exemples d'optimisations!

J'ai peur que de savoir ce que volatile sera et ne fera pas, vous devez passer par la documentation de votre compilateur.


2 commentaires

M'a fait regarder plus profondément dans les documents des vendeurs. J'ai trouvé cela dans la section décrivant le comportement défini de l'implémentation: "Ce qui constitue un accès à un objet qui a un type qualifié volatil (6.7.3)." - Toute référence à un objet de type volatile entraîne un accès. L'ordre dans lequel les objets volatils sont accessibles est défini par l'ordre exprimé dans le code source. Les références à des objets non volatils sont planifiés par ordre arbitraire, dans les contraintes données par les dépendances. (suivie d'un paragraphe sur la façon dont le passage d'un indicateur au compilateur fait de tout accès volatil une barrière de mémoire!)


@Andreas sur la question initiale: si vous avez un programme qui dépend de l'écriture à cette structure qui n'est pas fusionnée, il est peu probable que vous travailliez dans le domaine de l'iso portable C. D'un autre côté, il y a quelques utilisations requises de Volatile dans les programmes strictement conformes: utilisation de volatile SIG_ATOMIC_T dans un gestionnaire de signaux asynchrones, et en utilisant volatile sur les variables locales d'une fonction qui sont modifiées entre le contexte enregistré avec setjmp et restauré avec longjmp . En ce qui concerne la norme C, nous pouvons considérer volatile comme existant pour ces situations.