Que fait le mot clé volatile
? En C++, quel problème résout-il?
Dans mon cas, je n'en ai jamais sciemment eu besoin.
volatile
est nécessaire si vous lisez à partir d'un emplacement en mémoire dans lequel, disons, un processus/périphérique/tout ce qui peut être complètement écrit.
J'avais l'habitude de travailler avec un ram à deux ports dans un système multiprocesseur en ligne droite C. Nous avons utilisé une valeur 16 bits gérée par le matériel comme sémaphore pour savoir quand l'autre gars avait terminé. 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 définit jamais la valeur! Il est fou, débarrassez-vous de ce code!) Et mon code continuerait sans avoir acquis le sémaphore, causant des problèmes plus tard.
volatile
est nécessaire lors du développement de systèmes intégré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 accès ne sont pas optimisés par le compilateur.
Certains processeurs ont des registres à virgule flottante qui ont plus de 64 bits de précision (par exemple, x86 32 bits sans SSE, voir le commentaire de Peter). De cette façon, si vous exécutez plusieurs opérations sur des nombres à double précision, vous obtenez en fait 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 façon dont le compilateur a affecté les registres et fait 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 qui n'ont aucun sens algébrique mais réduisent l'erreur en virgule flottante, comme la sommation de Kahan. Algébriquement, c'est un nop, donc il sera souvent mal optimisé à moins que certaines variables intermédiaires ne soient volatiles.
Extrait d'un "Volatile as a promise" article de Dan Saks:
(...) un objet volatil est un objet dont la valeur peut changer spontanément. Autrement dit, lorsque vous déclarez un objet comme volatile, vous dites au compilateur que l'objet peut changer d'état même si aucune instruction du programme ne semble le changer. "
Voici des liens vers trois de ses articles concernant le mot clé volatile
:
Vous DEVEZ utiliser volatile lors de la mise en œuvre de structures de données sans verrouillage. Sinon, le compilateur est libre d'optimiser l'accès à la variable, ce qui changera la sémantique.
En d'autres termes, volatile indique au compilateur que les accès à 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
);
Une grande application sur laquelle je travaillais au début des années 1990 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 variables soient stockées dans des registres et effacées par le longjmp.
Dans la norme C, l'un des endroits où utiliser volatile
est avec un gestionnaire de signal. En fait, dans la norme C, tout ce que vous pouvez faire en toute sécurité dans un gestionnaire de signaux est de modifier une variable volatile sig_atomic_t
Ou de quitter rapidement. En effet, AFAIK, c'est le seul endroit en Standard C où l'utilisation de volatile
est requise pour éviter un comportement indéfini.
ISO/IEC 9899: 2011 §7.14.1.1 La fonction
signal
¶5 Si le signal se produit autrement qu'à la suite de l'appel de la fonction
abort
ouraise
, le comportement n'est pas défini si le gestionnaire de signal fait référence à tout objet avec une durée de stockage statique ou de thread qui n'est pas un objet atomique sans verrou autre qu'en affectant une valeur à un objet déclaré commevolatile sig_atomic_t
, ou le gestionnaire de signal appelle n'importe quelle fonction de la bibliothèque standard autre que la fonctionabort
, la fonction_Exit
, La fonctionquick_exit
Ou la fonctionsignal
avec le premier argument égal au numéro de signal correspondant au signal qui a provoqué l'appel du gestionnaire. De plus, si un tel appel à la fonctionsignal
entraîne un retour SIG_ERR, la valeur deerrno
est indéterminée.252)252) Si un signal est généré par un gestionnaire de signal asynchrone, le comportement n'est pas dé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 indulgent sur ce que vous pouvez faire dans un gestionnaire de signaux, mais il y a toujours 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é ).
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'attribut volatile (comme const) participe à la résolution de la surcharge.
template <typename T>
class Foo {
std::enable_if_t<sizeof(T)==4, void> f(T& t)
{ std::cout << 1 << t; }
void f(T volatile& t)
{ std::cout << 2 << const_cast<T&>(t); }
void bar() { T t; f(t); }
};
C'est légal; les deux surcharges sont potentiellement appelables et font presque la même chose. Le transtypage dans la surcharge volatile
est légal car nous savons que la barre ne passera pas un T
non volatile de toute façon. Cependant, la version volatile
est strictement pire, donc jamais choisie dans la résolution de surcharge si le f
non volatile est disponible.
Notez que le code ne dépend jamais réellement de l'accès à la mémoire volatile
.
Je l'ai utilisé dans les versions de débogage lorsque le compilateur insiste pour optimiser une variable que je veux pouvoir voir lorsque je parcours le code.
En 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 puisse le dire, la variable ne change jamais, donc elle optimise la vérification.
La même chose s'appliquerait à une variable qui peut être modifiée dans un thread différent dans un environnement plus traditionnel, mais là, nous faisons souvent des appels de synchronisation, donc le compilateur n'est pas aussi libre d'optimisation.
Le mot clé volatile
est destiné à empêcher le compilateur d'appliquer des optimisations sur des objets qui peuvent changer d'une manière qui ne peut pas être déterminée par le compilateur.
Les objets déclarés comme volatile
sont omis de l'optimisation car leurs valeurs peuvent être modifiées à tout moment par du code hors de la portée du code actuel. Le système lit toujours la valeur actuelle d'un objet volatile
à partir de l'emplacement de la mémoire plutôt que de conserver sa valeur dans le registre temporaire au point où il est demandé, même si une instruction précédente demandait une valeur au même objet.
Considérez les cas suivants
1) Variables globales modifiées par une routine de service d'interruption en dehors de la portée.
2) Variables globales dans une application multi-thread.
Si nous n'utilisons pas de qualificatif volatil, 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: le meilleur ami d'un programmeur
https://en.wikipedia.org/wiki/Volatile_ (computer_programming)
Votre programme semble fonctionner même sans volatile
mot clé? C'est peut-être la raison:
Comme mentionné précédemment, le mot clé volatile
aide pour des cas comme
volatile int* p = ...; // point to some memory
while( *p!=0 ) {} // loop until the memory becomes zero
Mais il semble qu'il n'y ait presque aucun effet une fois qu'une fonction externe ou non en ligne est appelée. Par exemple.:
while( *p!=0 ) { g(); }
Puis 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 l'optimiser. Mais lorsque le programme appelle un endroit où le compilateur ne peut pas voir ce qui se passe, il n'est plus sûr pour le compilateur de faire des hypothèses. Par conséquent, le compilateur générera du code qui lit toujours directement de la mémoire.
Mais méfiez-vous du jour, lorsque votre fonction g() devient inline (soit en raison de modifications explicites, soit en raison de l'habileté du compilateur/éditeur de liens), alors votre code peut se casser si vous oubliez le volatile
mot-clé!
Par conséquent, je recommande d'ajouter le mot clé volatile
même si votre programme semble fonctionner sans. Il rend l'intention plus claire et plus solide en ce qui concerne les changements futurs.
Dans les premiers jours de C, les compilateurs interprétaient toutes les actions qui lisent et écrivent des valeurs l comme des opérations de mémoire, à exécuter dans la même séquence que les lectures et les écritures apparaissaient dans le code. L'efficacité pourrait être considérablement 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 posait 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 l'ordre some, et donc le programmeur a choisi l'une des nombreuses alternatives tout aussi bonnes, ce n'était pas toujours le cas. Parfois, il serait important que certaines opérations se produisent dans une séquence particulière.
Les détails exacts du séquençage qui sont importants varient en fonction de la plate-forme cible et du domaine 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'accès est effectuée avec des valeurs l non qualifiées 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é devrait offrir toutes les garanties de commande supplémentaires qui pourraient être requises par le code ciblant sa plate-forme et son champ d'application prévus, sans avoir à utiliser une syntaxe non standard. .
Malheureusement, plutôt que d'identifier les garanties dont les programmeurs auraient besoin, de nombreux compilateurs ont plutôt opté pour offrir les garanties minimales nettes imposées par la norme. Cela rend volatile
beaucoup moins utile qu'il ne devrait l'être. Sur gcc ou clang, par exemple, un programmeur qui a besoin d'implémenter un "mutex de transfert" de base [un où une tâche qui a acquis et publié un mutex ne le fera plus jusqu'à ce que l'autre tâche l'ait fait] doit en faire un de quatre choses:
Mettez 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 Whole Program Optimization.
Qualifiez tous les objets gardés par le mutex comme volatile
-- quelque chose qui ne devrait pas être nécessaire si tous les accès se produisent après l'acquisition du mutex et avant de le libérer.
Utilisez le niveau d'optimisation 0 pour forcer le compilateur à générer du code comme si tous les objets non qualifiés register
sont volatile
.
Utilisez des directives spécifiques à gcc.
En revanche, lorsque vous utilisez un compilateur de meilleure qualité qui convient mieux à la programmation de systèmes, comme icc, on aurait une autre option:
volatile
est effectuée chaque fois qu'une acquisition ou une libération est nécessaire.L'acquisition d'un "mutex de transfert" de base nécessite une lecture de volatile
(pour voir s'il est prêt), et ne devrait pas non plus nécessiter une écriture de volatile
(l'autre côté n'essaiera pas de le ré-acquérir jusqu'à ce qu'il soit restitué) mais avoir à effectuer une écriture volatile
vide de sens est toujours mieux que n'importe laquelle des options disponibles sous gcc ou clang.
Outre le fait que le mot clé volatile est utilisé pour dire au compilateur de ne pas optimiser l'accès à certaines variables (qui peuvent être modifiées par un thread ou une routine d'interruption), il peut également être tilisé pour supprimer certains bogues du compilateur - OUI cela peut être ---.
Par exemple, j'ai travaillé sur une plate-forme embarquée où le compilateur faisait de fausses hypothèses concernant la valeur d'une variable. Si le code n'était pas optimisé, le programme fonctionnerait correctement. Avec des optimisations (qui étaient vraiment nécessaires car c'était une routine critique), le code ne fonctionnerait pas correctement. La seule solution (mais pas très correcte) était de déclarer la variable "défectueuse" comme volatile.
Une utilisation que je dois vous rappeler est que, dans la fonction de gestion du signal, si vous souhaitez accéder/modifier une variable globale (par exemple, la marquer comme exit = true), vous devez déclarer cette variable comme "volatile".