Après avoir examiné un tas d' autres questions et leurs réponses , j'ai l'impression qu'il n'y a pas de consensus sur ce que signifie exactement le mot clé "volatile" en C.
Même la norme elle-même ne semble pas assez claire pour que tout le monde s'entende sur ce qu'elle signifie .
Entre autres problèmes:
- Il semble offrir différentes garanties en fonction de votre matériel et de votre compilateur.
- Cela affecte les optimisations du compilateur mais pas les optimisations matérielles, donc sur un processeur avancé qui fait ses propres optimisations au moment de l'exécution, il n'est même pas clair si le compilateur peut empêcher toute optimisation que vous souhaitez empêcher. (Certains compilateurs génèrent des instructions pour empêcher certaines optimisations matérielles sur certains systèmes, mais cela ne semble en aucune façon standardisé.)
Pour résumer le problème, il apparaît (après avoir lu beaucoup) que "volatile" garantit quelque chose comme: La valeur sera lue / écrite non seulement depuis / vers un registre, mais au moins vers le cache L1 du noyau, dans le même ordre que les lectures / écritures apparaissent dans le code. Mais cela semble inutile, car la lecture / écriture depuis / vers un registre est déjà suffisante dans le même thread, tandis que la coordination avec le cache L1 ne garantit rien de plus concernant la coordination avec les autres threads. Je ne peux pas imaginer quand il pourrait être important de synchroniser uniquement avec le cache L1.
UTILISATION 1
La seule utilisation largement acceptée de volatile semble être pour les systèmes anciens ou intégrés où certains emplacements de mémoire sont mappés matériellement aux fonctions d'E / S, comme un bit en mémoire qui contrôle (directement, dans le matériel) une lumière , ou un peu en mémoire qui vous indique si une touche du clavier est enfoncée ou non (car elle est connectée directement par le matériel à la touche).
Il semble que «utiliser 1» ne se produit pas dans le code portable dont les cibles incluent les systèmes multicœurs.
UTILISATION 2 La
mémoire qui peut être lue ou écrite à tout moment par un gestionnaire d'interruption (qui peut contrôler une lumière ou stocker des informations à partir d'une clé) n'est pas trop différente de "utiliser 1". Mais déjà pour cela, nous avons le problème que, selon le système, le gestionnaire d'interruption peut s'exécuter sur un cœur différent avec son propre cache mémoire , et "volatile" ne garantit pas la cohérence du cache sur tous les systèmes.
Donc, "utiliser 2" semble aller au-delà de ce que "volatile" peut offrir.
UTILISATION 3
La seule autre utilisation incontestée que je vois est d'empêcher une mauvaise optimisation des accès via différentes variables pointant vers la même mémoire que le compilateur ne réalise pas est la même mémoire. Mais cela n'est probablement incontesté que parce que les gens n'en parlent pas - je n'en ai vu qu'une mention. Et je pensais que la norme C reconnaissait déjà que des pointeurs "différents" (comme différents arguments vers une fonction) pouvaient pointer vers le même élément ou des éléments voisins, et j'ai déjà spécifié que le compilateur devait produire du code qui fonctionne même dans de tels cas. Cependant, je n'ai pas pu trouver rapidement ce sujet dans la dernière norme (500 pages!).
Donc, "utiliser 3" n'existe peut-être pas du tout?
D'où ma question:
Est-ce que "volatile" garantit quoi que ce soit dans le code C portable pour les systèmes multicœurs?
EDIT - mise à jour
Après avoir parcouru la dernière norme , il semble que la réponse soit au moins un oui très limité:
1. La norme spécifie à plusieurs reprises un traitement spécial pour le type spécifique "sig_atomic_t volatile". Cependant, la norme indique également que l'utilisation de la fonction de signal dans un programme multithread entraîne un comportement indéfini. Ce cas d'utilisation semble donc limité à la communication entre un programme monothread et son gestionnaire de signaux.
2. La norme spécifie également une signification claire pour "volatile" par rapport à setjmp / longjmp. (Un exemple de code là où c'est important est donné dans d'autres questions et réponses .)
La question la plus précise devient donc:
«volatile» garantit-il quoi que ce soit dans le code C portable pour les systèmes multicœurs, à l'exception de (1) autoriser un programme à thread unique à recevoir des informations de son gestionnaire de signal, ou (2) autoriser setjmp code pour voir les variables modifiées entre setjmp et longjmp?
C'est toujours une question oui / non.
Si "oui", ce serait bien si vous pouviez montrer un exemple de code portable sans bogue qui devient bogué si "volatile" est omis. Si "non", alors je suppose qu'un compilateur est libre d'ignorer "volatile" en dehors de ces deux cas très spécifiques, pour les cibles multicœurs.
volatile
d'informer le programme qu'il peut changer de manière asynchrone.volatile
précisément ce qui, à mon avis, est nécessaire.Réponses:
Non, ce n'est absolument pas le cas . Et cela rend volatile presque inutile aux fins du code sûr MT.
Si c'était le cas, alors volatile serait assez bon pour les variables partagées par plusieurs threads, car la commande des événements dans le cache L1 est tout ce que vous devez faire dans un processeur typique (c'est-à-dire multicœur ou multi-processeur sur la carte mère) capable de coopérer d'une manière qui rend possible une implémentation normale du multithreading C / C ++ ou Java avec des coûts attendus typiques (c'est-à-dire pas un coût énorme pour la plupart des opérations mutex atomiques ou non satisfaites).
Mais volatile ne pas fournir toute commande garantie (ou « la visibilité de la mémoire ») dans le cache ni en théorie ni en pratique.
(Remarque: ce qui suit est basé sur une bonne interprétation des documents standard, l'intention de la norme, la pratique historique et une compréhension approfondie des attentes des rédacteurs du compilateur. Cette approche basée sur l'histoire, les pratiques réelles, les attentes et la compréhension des personnes réelles dans le monde réel, qui est beaucoup plus fort et plus fiable que l'analyse des mots d'un document qui n'est pas connu pour être une écriture stellaire et qui a été révisé plusieurs fois.)
En pratique, volatile garantit ptrace-capacité qui est la capacité d'utiliser les informations de débogage pour le programme en cours d'exécution, à n'importe quel niveau d'optimisation , et le fait que les informations de débogage ont un sens pour ces objets volatils:
ptrace
(un mécanisme semblable à ptrace) pour définir des points d'arrêt significatifs aux points de séquence après des opérations impliquant des objets volatils: vous pouvez vraiment interrompre exactement ces points (notez que cela ne fonctionne que si vous êtes prêt à définir de nombreux points d'arrêt comme n'importe quel L'instruction C / C ++ peut être compilée à de nombreux points de début et de fin d'assemblage différents, comme dans une boucle massivement déroulée);La volatilité garantit en pratique un peu plus que l'interprétation stricte de ptrace: elle garantit également que les variables automatiques volatiles ont une adresse sur la pile, car elles ne sont pas affectées à un registre, une allocation de registre qui rendrait les manipulations de ptrace plus délicates (le compilateur peut sortie des informations de débogage pour expliquer comment les variables sont allouées aux registres, mais la lecture et la modification de l'état du registre sont légèrement plus complexes que l'accès aux adresses mémoire).
Notez que la capacité de débogage complète du programme, qui considère toutes les variables volatiles au moins aux points de séquence, est fournie par le mode "d'optimisation zéro" du compilateur, un mode qui effectue toujours des optimisations triviales comme des simplifications arithmétiques (il n'y a généralement aucune garantie non optimisation à tous les modes). Mais volatile est plus fort que non optimisation:
x-x
peut être simplifié pour un entier non volatilex
mais pas pour un objet volatil.Donc, la volatilité signifie qu'il est garanti d'être compilé tel quel, comme la traduction de la source en binaire / assembleur par le compilateur d'un appel système n'est pas une réinterprétation, modifiée ou optimisée de quelque façon par un compilateur. Notez que les appels de bibliothèque peuvent ou non être des appels système. De nombreuses fonctions système officielles sont en fait des fonctions de bibliothèque qui offrent une fine couche d'interposition et s'en remettent généralement au noyau à la fin. (En particulier,
getpid
n'a pas besoin d'aller au noyau et pourrait bien lire un emplacement mémoire fourni par le système d'exploitation contenant les informations.)Les interactions volatiles sont des interactions avec le monde extérieur de la machine réelle , qui doit suivre la "machine abstraite". Ce ne sont pas des interactions internes des parties de programme avec d'autres parties de programme. Le compilateur ne peut que raisonner sur ce qu'il sait, à savoir les parties internes du programme.
La génération de code pour un accès volatil doit suivre l'interaction la plus naturelle avec cet emplacement mémoire: cela ne devrait pas être surprenant. Cela signifie que certains accès volatils devraient être atomiques : si la façon naturelle de lire ou d'écrire la représentation d'un
long
sur l'architecture est atomique, alors il est prévu qu'une lecture ou une écriture d'unvolatile long
sera atomique, car le compilateur ne devrait pas générer un code stupide et inefficace pour accéder aux objets volatils octet par octet, par exemple .Vous devriez pouvoir le déterminer en connaissant l'architecture. Vous n'avez rien à savoir sur le compilateur, car volatile signifie que le compilateur doit être transparent .
Mais volatile ne fait que forcer l'émission d'assemblage attendu pour les moins optimisés pour des cas particuliers à faire une opération mémoire: la sémantique volatile signifie la sémantique générale du cas.
Le cas général est ce que fait le compilateur lorsqu'il n'a aucune information sur une construction: f.ex. appeler une fonction virtuelle sur une valeur l via la répartition dynamique est un cas général, faire un appel direct au dépassement après avoir déterminé au moment de la compilation le type de l'objet désigné par l'expression est un cas particulier. Le compilateur a toujours une gestion générale des cas de toutes les constructions, et il suit l'ABI.
Volatile ne fait rien de spécial pour synchroniser les threads ou fournir une "visibilité mémoire": volatile ne fournit que des garanties au niveau abstrait vu de l'intérieur d'un thread en cours d'exécution ou arrêté, c'est-à-dire à l'intérieur d'un coeur de CPU :
Seul le deuxième point signifie que volatile n'est pas utile dans la plupart des problèmes de communication entre les threads; le premier point est essentiellement hors de propos dans tout problème de programmation qui n'implique pas de communication avec des composants matériels en dehors des CPU mais toujours sur le bus mémoire.
La propriété volatile fournissant un comportement garanti du point de vue du noyau exécutant le thread signifie que les signaux asynchrones délivrés à ce thread, qui sont exécutés du point de vue de l'ordre d'exécution de ce thread, voir les opérations dans l'ordre du code source .
Sauf si vous prévoyez d'envoyer des signaux à vos threads (une approche extrêmement utile pour la consolidation des informations sur les threads en cours d'exécution sans point d'arrêt convenu précédemment), volatile n'est pas pour vous.
la source
Je ne suis pas un expert, mais cppreference.com a ce qui me semble être de très bonnes informations
volatile
. Voici l'essentiel:Il donne également quelques utilisations:
Et bien sûr, il mentionne que ce
volatile
n'est pas utile pour la synchronisation des threads:la source
longjmp
en code C ++.Tout d'abord, il y a eu historiquement divers hoquets concernant différentes interprétations de la signification de l'
volatile
accès et similaires. Voir cette étude: Les volatiles sont mal compilés et que faire à ce sujet .Outre les divers problèmes mentionnés dans cette étude, le comportement de
volatile
est portable, sauf pour un aspect d'entre eux: lorsqu'ils agissent comme des barrières de mémoire . Une barrière de mémoire est un mécanisme qui est là pour empêcher l'exécution simultanée non séquencée de votre code. L'utilisationvolatile
comme barrière mémoire n'est certainement pas portable.Que le langage C garantisse ou non le comportement de la mémoire
volatile
est apparemment discutable, bien que personnellement, je pense que le langage est clair. Nous avons d'abord la définition formelle des effets secondaires, C17 5.1.2.3:La norme définit le terme séquençage comme un moyen de déterminer l'ordre d'évaluation (exécution). La définition est formelle et lourde:
Le TL; DR de ce qui précède est fondamentalement que dans le cas où nous avons une expression
A
qui contient des effets secondaires, elle doit être exécutée avant une autre expressionB
, au cas où elleB
serait séquencée aprèsA
.Les optimisations du code C sont rendues possibles grâce à cette partie:
Cela signifie que le programme peut évaluer (exécuter) des expressions dans l'ordre que la norme impose ailleurs (ordre d'évaluation, etc.). Mais il n'a pas besoin d'évaluer (d'exécuter) une valeur s'il peut en déduire qu'elle n'est pas utilisée. Par exemple, l'opération
0 * x
n'a pas besoin d'évaluerx
et de simplement remplacer l'expression par0
.Sauf si l' accès à une variable est un effet secondaire. Ce qui signifie que dans le cas
x
estvolatile
, il doit évaluer (exécuter) ,0 * x
même si le résultat sera toujours 0. L' optimisation n'est pas autorisé.De plus, la norme parle de comportement observable:
Compte tenu de tout ce qui précède, une implémentation conforme (compilateur + système sous-jacent) peut ne pas exécuter l'accès des
volatile
objets dans un ordre non séquencé, au cas où la sémantique de la source C écrite dirait le contraire.Cela signifie que dans cet exemple
Les deux expressions d'affectation doivent être évaluées et
z = x;
doivent être évaluées avantz = y;
. Une implémentation multiprocesseur qui sous-traite ces deux opérations à deux cœurs de séquences différentes n'est pas conforme!Le dilemme est que les compilateurs ne peuvent pas faire grand-chose sur des choses comme la mise en cache de pré-extraction et le pipelining d'instructions, etc., en particulier pas lorsqu'ils s'exécutent sur un système d'exploitation. Et donc les compilateurs remettent ce problème aux programmeurs, en leur disant que les barrières mémoire sont désormais la responsabilité du programmeur. Alors que la norme C indique clairement que le problème doit être résolu par le compilateur.
Le compilateur ne se soucie pas nécessairement de résoudre le problème, et donc
volatile
pour agir comme une barrière de mémoire, il n'est pas portable. C'est devenu un problème de qualité de mise en œuvre.la source
z
seraient-elles réellement exécutées? (commez = x; z = y;
) La valeur va être effacée dans la prochaine instruction.z
vraiment attribué deux fois? Comment savez-vous que "les lectures sont exécutées"?