Pourquoi le volatile existe-t-il?

Que fait le mot clé volatile ? En C ++, quel problème résout-il?

Dans mon cas, je n’ai jamais sciemment besoin de cela.

volatile est nécessaire si vous lisez à partir d’un endroit en mémoire que, par exemple, un processus / périphérique / quoi que ce soit complètement différent.

J’avais l’habitude de travailler avec un port double-ram dans un système multiprocesseur en langage C. Nous avons utilisé une valeur de 16 bits avec gestion matérielle comme sémaphore pour savoir quand l’autre a été fait. Essentiellement, nous avons fait ceci:

 void waitForSemaphore() { volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/ while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED); } 

Sans volatile , l’optimiseur considère la boucle comme inutile (le gars ne met jamais la valeur! Il est fou, élimine ce code!) Et mon code continuerait sans avoir acquis le sémaphore, ce qui causerait plus tard des problèmes.

volatile est nécessaire lors du développement de systèmes embarqués ou de pilotes de périphériques, où vous devez lire ou écrire un périphérique matériel mappé en mémoire. Le contenu d’un registre de périphérique particulier peut changer à tout moment. Vous avez donc besoin du mot-clé volatile pour vous assurer que ces compilateurs n’optimisent pas ces access.

La plupart des processeurs modernes ont des registres à virgule flottante qui ont plus de 64 bits de précision. Ainsi, si vous exécutez plusieurs opérations sur des nombres à double précision, vous obtenez une réponse plus précise que si vous deviez tronquer chaque résultat intermédiaire à 64 bits.

C’est généralement génial, mais cela signifie qu’en fonction de la manière dont le compilateur a affecté les registres et les optimisations, vous obtiendrez des résultats différents pour les mêmes opérations sur les mêmes entrées. Si vous avez besoin de cohérence, vous pouvez forcer chaque opération à revenir en mémoire en utilisant le mot-clé volatile.

Il est également utile pour certains algorithmes sans sens algébrique, mais qui réduisent les erreurs en virgule flottante, telles que la sum de Kahan. Algébriquement, c’est un nop, donc il sera souvent mal optimisé à moins que certaines variables intermédiaires soient volatiles.

Article d’un système embarqué par Dan Saks:

“Un object volatil est un object dont la valeur peut changer spontanément. En d’autres termes, lorsque vous déclarez un object volatile, vous dites au compilateur que l’object peut changer d’état même si aucune instruction du programme ne semble la modifier.”

Liens vers deux excellents articles de M. Saks concernant le mot-clé volatile:

http://www.embedded.com/columns/programmingpointers/174300478 http://www.embedded.com/columns/programmingpointers/175801310

Vous DEVEZ utiliser volatile lorsque vous implémentez des structures de données sans locking. Sinon, le compilateur est libre d’optimiser l’access à la variable, ce qui changera la sémantique.

En d’autres termes, volatile indique au compilateur que les access à cette variable doivent correspondre à une opération de lecture / écriture de la mémoire physique.

Par exemple, voici comment InterlockedIncrement est déclaré dans l’API Win32:

 LONG __cdecl InterlockedIncrement( __inout LONG volatile *Addend ); 

Dans Standard C, l’un des endroits où utiliser volatile est d’utiliser un gestionnaire de signal. En fait, dans Standard C, tout ce que vous pouvez faire en toute sécurité dans un gestionnaire de signaux consiste à modifier une variable volatile sig_atomic_t ou à quitter rapidement. En effet, AFAIK, c’est le seul endroit de la norme C où l’utilisation de volatile est nécessaire pour éviter un comportement indéfini.

ISO / IEC 9899: 2011 §7.14.1.1 La fonction de signal

¶5 Si le signal survient autrement que lors de l’appel de la fonction d’ abort ou de raise , le comportement n’est pas défini si le gestionnaire de signal fait référence à un object avec une durée de stockage statique ou non autre que l’atsortingbution une valeur à un object déclaré volatile sig_atomic_t , ou le gestionnaire de signal appelle une fonction autre que la fonction d’ _Exit , la fonction quick_exit , la fonction quick_exit ou la fonction de signal avec le premier argument égal au numéro de signal correspondant à le signal qui a provoqué l’invocation du gestionnaire. De plus, si un tel appel à la fonction signal produit un retour SIG_ERR, la valeur de errno est indéterminée. 252)

252) Si un signal est généré par un gestionnaire de signal asynchrone, le comportement est indéfini.

Cela signifie que dans la norme C, vous pouvez écrire:

 static volatile sig_atomic_t sig_num = 0; static void sig_handler(int signum) { signal(signum, sig_handler); sig_num = signum; } 

et pas grand chose d’autre.

POSIX est beaucoup plus indulgente sur ce que vous pouvez faire dans un gestionnaire de signal, mais il existe encore des limitations (et l’une des limitations est que la bibliothèque d’E / S standard – printf() et al – ne peut pas être utilisée en toute sécurité).

Une application volumineuse sur laquelle je travaillais au début des années 90 contenait la gestion des exceptions basées sur C à l’aide de setjmp et longjmp. Le mot-clé volatile était nécessaire sur les variables dont les valeurs devaient être conservées dans le bloc de code qui servait de clause “catch”, de peur que ces vars soient stockés dans des registres et effacés par le longjmp.

Développant pour un embarqué, j’ai une boucle qui vérifie une variable qui peut être modifiée dans un gestionnaire d’interruption. Sans “volatile”, la boucle devient un noop – pour autant que le compilateur le sache, la variable ne change jamais, ce qui optimise le contrôle.

La même chose s’appliquerait à une variable qui peut être modifiée dans un thread différent dans un environnement plus traditionnel, mais nous faisons souvent des appels de synchronisation, de sorte que le compilateur n’est pas si gratuit avec l’optimisation.

Je l’ai utilisé dans les versions de débogage lorsque le compilateur insiste pour optimiser une variable que je veux pouvoir voir au fur et à mesure que je passe en revue le code.

  1. vous devez l’utiliser pour implémenter des spinlocks ainsi que certaines (toutes?) structures de données sans verrou
  2. l’utiliser avec des opérations / instructions atomiques
  3. m’a aidé une fois pour surmonter le bogue du compilateur (code généré de manière incorrecte pendant l’optimisation)

En plus de l’utiliser comme prévu, volatile est utilisé dans la métaprogrammation (modèle). Il peut être utilisé pour éviter une surcharge accidentelle, car l’atsortingbut volatile (comme const) participe à la résolution de la surcharge.

 template  class Foo { std::enable_if_t f(T& t) { std::cout < < 1 << t; } void f(T volatile& t) { std::cout << 2 << const_cast(t); } void bar() { T t; f(t); } }; 

Ceci est légal les deux surcharges sont potentiellement appelables et presque identiques. Le casting dans la surcharge volatile est légal car nous soaps que les bars ne passeront pas un T non volatile. La version volatile est ssortingctement pire, donc, jamais choisie en résolution de surcharge si le non-volatile est disponible.

Notez que le code ne dépend jamais réellement de l’access à la mémoire volatile .

Le mot-clé volatile est destiné à empêcher le compilateur d’appliquer des optimisations sur des objects qui peuvent changer de manière indétectable par le compilateur.

Les objects déclarés comme volatile sont omis de l’optimisation car leurs valeurs peuvent être modifiées à tout moment par du code en dehors de la scope du code actuel. Le système lit toujours la valeur actuelle d’un object volatile partir de l’emplacement de la mémoire plutôt que de conserver sa valeur dans le registre temporaire au moment où il est demandé, même si une instruction précédente demandait une valeur du même object.

Considérez les cas suivants

1) Variables globales modifiées par une routine de service d’interruption en dehors de la scope.

2) Variables globales dans une application multithread.

Si nous n’utilisons pas de qualificatif volatile, les problèmes suivants peuvent survenir

1) Le code peut ne pas fonctionner comme prévu lorsque l’optimisation est activée.

2) Le code peut ne pas fonctionner comme prévu lorsque les interruptions sont activées et utilisées.

Volatile: Un meilleur ami du programmeur

https://en.wikipedia.org/wiki/Volatile_(computer_programming)

Outre le fait que le mot-clé volatile est utilisé pour indiquer au compilateur de ne pas optimiser l’access à certaines variables (pouvant être modifiées par un thread ou une routine d’interruption), il peut également être utilisé pour supprimer certains bogues du compilateur . être —.

Par exemple, j’ai travaillé sur une plate-forme intégrée si le compilateur faisait des hypothèses erronées concernant la valeur d’une variable. Si le code n’était pas optimisé, le programme fonctionnerait correctement. Avec les optimisations (qui étaient vraiment nécessaires car c’était une routine critique), le code ne fonctionnerait pas correctement. La seule solution (bien que pas très correcte) était de déclarer la variable «défectueuse» comme volatile.

Je devrais vous rappeler une chose: dans la fonction de gestionnaire de signal, si vous voulez accéder / modifier une variable globale (par exemple, la marquer comme exit = true), vous devez déclarer cette variable comme «volatile».

Votre programme semble fonctionner même sans mot-clé volatile ? Peut-être que c’est la raison:

Comme mentionné précédemment, le mot-clé volatile aide pour les cas comme

 volatile int* p = ...; // point to some memory while( *p!=0 ) {} // loop until the memory becomes zero 

Mais il semble y avoir quasiment aucun effet une fois qu’une fonction externe ou non intégrée est appelée. Par exemple:

 while( *p!=0 ) { g(); } 

Ensuite, avec ou sans volatile presque le même résultat est généré.

Tant que g () peut être complètement intégré, le compilateur peut voir tout ce qui se passe et peut donc optimiser. Mais lorsque le programme appelle un endroit où le compilateur ne voit pas ce qui se passe, le compilateur ne peut plus faire d’hypothèses. Par conséquent, le compilateur générera du code qui lit toujours directement depuis la mémoire.

Mais méfiez-vous de la journée, lorsque votre fonction g () devient en ligne (soit en raison de modifications explicites, soit en raison de l’intelligence du compilateur / éditeur de liens), votre code risque de se briser si vous avez oublié le mot clé volatile !

Par conséquent, je recommande d’append le mot clé volatile même si votre programme semble fonctionner sans. Cela rend l’intention plus claire et plus robuste en ce qui concerne les changements futurs.

Dans les premiers temps de C, les compilateurs interprètent toutes les actions lisant et écrivant des lvalues ​​comme des opérations de mémoire, à exécuter dans le même ordre que les lectures et écritures apparaissant dans le code. L’efficacité pourrait être grandement améliorée dans de nombreux cas si les compilateurs disposaient d’une certaine liberté pour réorganiser et consolider les opérations, mais cela présentait un problème. Même les opérations étaient souvent spécifiées dans un certain ordre simplement parce qu’il était nécessaire de les spécifier dans un certain ordre, et le programmeur choisissait donc l’une des nombreuses alternatives tout aussi bonnes, ce qui n’était pas toujours le cas. Parfois, il serait important que certaines opérations se produisent dans une séquence particulière.

La précision des détails du séquencement dépendra de la plate-forme cible et du champ d’application. Plutôt que de fournir un contrôle particulièrement détaillé, le Standard a opté pour un modèle simple: si une séquence d’access se fait avec des lvalues ​​non qualifiées de volatile , un compilateur peut les réorganiser et les consolider comme bon lui semble. Si une action est effectuée avec une valeur qualifiée volatile , une implémentation de qualité doit offrir les garanties de commande supplémentaires requirejses par le code ciblant la plate-forme et le champ d’application prévus, sans avoir à recourir à une syntaxe non standard.

Malheureusement, plutôt que d’identifier les garanties dont les programmeurs auraient besoin, de nombreux compilateurs ont plutôt choisi d’offrir les garanties minimales requirejses par la norme. Cela rend la volatile beaucoup moins utile qu’elle ne devrait l’être. Sur gcc ou clang, par exemple, un programmeur ayant besoin d’implémenter un “mutex de transfert” de base [dans lequel une tâche qui a acquis et libéré un mutex ne le fera plus tant que l’autre tâche ne l’a pas fait] de quatre choses:

  1. Placez l’acquisition et la libération du mutex dans une fonction que le compilateur ne peut pas incorporer et à laquelle il ne peut pas appliquer l’optimisation complète du programme.

  2. Qualifiez tous les objects protégés par le mutex comme volatile quelque chose qui ne devrait pas être nécessaire si tous les access se produisent après avoir acquis le mutex et avant de le libérer.

  3. Utilisez le niveau d’optimisation 0 pour forcer le compilateur à générer du code comme si tous les objects non qualifiés de register étaient volatile .

  4. Utilisez les directives spécifiques à gcc.

En revanche, lorsqu’on utilise un compilateur de meilleure qualité, plus adapté à la programmation de systèmes, comme icc, on aurait une autre option:

  1. Assurez-vous qu’une écriture qualifiée volatile est effectuée partout où une acquisition ou une libération est nécessaire.

L’acquisition d’un “mutex de transfert” de base nécessite une lecture volatile (pour voir si elle est prête), et ne devrait pas nécessiter une écriture volatile (l’autre partie n’essaiera pas de la réacquérir avant de la rendre) mais devoir effectuer une écriture volatile sans signification est toujours préférable à l’une des options disponibles sous gcc ou clang.