Pourquoi est-ce que volatile
est nécessaire en C? A quoi cela sert? Que va-t-il faire?
Volatile indique au compilateur de ne pas optimiser tout ce qui a trait à la variable volatile.
Il existe au moins trois raisons courantes de l'utiliser, toutes impliquant des situations dans lesquelles la valeur de la variable peut changer sans action du code visible: lorsque vous vous connectez à un matériel qui modifie la valeur elle-même; quand il y a un autre thread en cours qui utilise également la variable; ou quand il y a un gestionnaire de signal qui pourrait changer la valeur de la variable.
Supposons que vous avez un petit élément de matériel mappé dans RAM quelque part et qui possède deux adresses: un port de commande et un port de données:
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
Maintenant, vous voulez envoyer une commande:
void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
Cela semble facile, mais cela peut échouer car le compilateur est libre de changer l'ordre dans lequel les données et les commandes sont écrites. Cela entraînerait notre petit gadget à émettre des commandes avec la valeur de données précédente. Jetez également un coup d'œil à la boucle d'attente en cas d'occupation. Celui-ci sera optimisé. Le compilateur essaiera d’être intelligent, lira une seule fois la valeur d’isbusy puis passera dans une boucle infinie. Ce n'est pas ce que tu veux.
Pour résoudre ce problème, vous devez déclarer le gadget de pointeur volatil. De cette façon, le compilateur est obligé de faire ce que vous avez écrit. Il ne peut pas supprimer les assignations de mémoire, il ne peut pas mettre les variables dans les registres en cache et il ne peut pas non plus changer l'ordre des assignations:
Ceci est la version correcte:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
volatile
in C a été créé pour ne pas mettre automatiquement en cache les valeurs de la variable. Cela indiquera au compilateur de ne pas mettre en cache la valeur de cette variable. Donc, il va générer du code pour prendre la valeur de la variable donnée volatile
de la mémoire principale à chaque fois qu'elle la rencontre. Ce mécanisme est utilisé car à tout moment, la valeur peut être modifiée par le système d'exploitation ou par toute interruption. Donc, utiliser volatile
nous aidera à accéder à la valeur à chaque fois.
Une autre utilisation de volatile
est celle des gestionnaires de signaux. Si vous avez un code comme celui-ci:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Le compilateur est autorisé à remarquer que le corps de la boucle ne touche pas la variable quit
et ne convertit pas la boucle en une boucle while (true)
. Même si la variable quit
est définie sur le gestionnaire de signaux pour SIGINT
et SIGTERM
; le compilateur n'a aucun moyen de le savoir.
Cependant, si la variable quit
est déclarée volatile
, le compilateur est obligé de la charger à chaque fois, car elle peut être modifiée ailleurs. C'est exactement ce que vous voulez dans cette situation.
volatile
indique au compilateur que votre variable peut être modifiée par d'autres moyens que le code qui y accède. Par exemple, il peut s'agir d'un emplacement de mémoire mappé E/S. Si cela n'est pas spécifié dans de tels cas, certains accès variables peuvent être optimisés, par exemple, son contenu peut être conservé dans un registre, et l'emplacement de la mémoire ne peut pas être relu.
Voir cet article de Andrei Alexandrescu, " volatile - Meilleur ami du programmeur multithread) "
Le volatil mot clé a été conçu pour empêcher les optimisations du compilateur susceptibles de rendre le code incorrect en présence de certains événements asynchrones. Par exemple, si vous déclarez une variable primitive en tant que volatil, le compilateur n’est pas autorisé à la mettre en cache dans un registre - une optimisation commune qui aurait des conséquences désastreuses si cette variable était partagée entre plusieurs threads. Donc, en règle générale, si vous avez des variables de type primitif qui doivent être partagées entre plusieurs threads, déclarez ces variables volatil. Mais vous pouvez en réalité faire beaucoup plus avec ce mot clé: vous pouvez l'utiliser pour intercepter du code qui n'est pas thread-safe, et vous pouvez le faire au moment de la compilation. Cet article montre comment c'est fait. la solution implique un simple pointeur intelligent qui facilite également la sérialisation des sections critiques de code.
L'article s'applique à C
et C++
.
Voir également l'article " C++ et les dangers du verrouillage à double contrôle " de Scott Meyers et Andrei Alexandrescu:
Ainsi, lorsque vous traitez avec certains emplacements de mémoire (par exemple, des ports mappés en mémoire ou une mémoire référencée par ISR [Interrupt Service Routines]), certaines optimisations doivent être suspendues. volatile existe pour spécifier un traitement spécial pour de tels emplacements, notamment: (1) le contenu d’une variable volatile est "instable" (peut changer par des moyens inconnus du compilateur), (2) toutes les écritures dans des données volatiles sont "observables"; doivent être exécutés religieusement, et (3) toutes les opérations sur des données volatiles sont exécutées dans l'ordre dans lequel elles apparaissent dans le code source. Les deux premières règles garantissent une lecture et une écriture correctes. Le dernier permet l'implémentation de protocoles d'E/S combinant entrée et sortie. C’est officieusement ce que garantissent les volatiles C et C++.
Mon explication simple est:
Dans certains scénarios, en fonction de la logique ou du code, le compilateur optimisera les variables dont il pense qu'elles ne changent pas. Le mot clé volatile
empêche une variable d'être optimisée.
Par exemple:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
A partir du code ci-dessus, le compilateur peut penser que usb_interface_flag
est défini sur 0 et que, dans la boucle while, il sera nul pour toujours. Après l'optimisation, le compilateur le traitera comme while(true)
tout le temps, ce qui entraînera une boucle infinie.
Pour éviter ce genre de scénario, nous déclarons l’indicateur volatil, nous disons au compilateur que cette valeur peut être modifiée par une interface externe ou un autre module de programme, c’est-à-dire, ne l’optimisez pas. C'est le cas d'utilisation de volatile.
Une utilisation marginale pour volatile est la suivante. Supposons que vous vouliez calculer la dérivée numérique d'une fonction f
:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
Le problème est que x+h-x
n'est généralement pas égal à h
en raison d'erreurs d'arrondi. Pensez-y: lorsque vous soustrayez des nombres très proches, vous perdez beaucoup de chiffres significatifs qui peuvent gâcher le calcul de la dérivée (pensez à 1.00001 - 1). Une solution de contournement possible pourrait être
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
mais selon votre plate-forme et les commutateurs du compilateur, la deuxième ligne de cette fonction peut être effacée par un compilateur optimisant de manière agressive. Donc, vous écrivez à la place
volatile double hh = x + h;
hh -= x;
pour forcer le compilateur à lire l'emplacement de la mémoire contenant hh, annulant ainsi une éventuelle opportunité d'optimisation.
Il y a deux utilisations. Ceux-ci sont spécialement utilisés plus souvent dans le développement intégré.
Le compilateur n'optimisera pas les fonctions utilisant des variables définies avec le mot clé volatile
Volatile est utilisé pour accéder aux emplacements mémoire exacts dans la RAM, la ROM, etc.
Voir les exemples avec la liste de montage. objet: utilisation du mot clé "volatile" C dans le développement intégré
Volatile est également utile lorsque vous souhaitez forcer le compilateur à ne pas optimiser une séquence de code spécifique (par exemple, pour écrire un micro-repère).
Je mentionnerai un autre scénario dans lequel les substances volatiles sont importantes.
Supposons que vous mappiez en mémoire un fichier pour une E/S plus rapide et que ce fichier puisse être modifié en arrière-plan (par exemple, le fichier ne se trouve pas sur votre disque dur local, mais est plutôt servi sur le réseau par un autre ordinateur).
Si vous accédez aux données du fichier mappé en mémoire via des pointeurs sur des objets non volatils (au niveau du code source), le code généré par le compilateur peut extraire les mêmes données plusieurs fois sans que vous en soyez conscient.
Si ces données changent, il est possible que votre programme utilise plusieurs versions différentes des données et entre dans un état incohérent. Cela peut entraîner non seulement un comportement logiquement incorrect du programme, mais également des failles de sécurité exploitables s’il traite des fichiers non fiables ou des fichiers situés à des emplacements non fiables.
Si vous vous souciez de la sécurité, et vous devriez, c'est un scénario important à considérer.
volatile signifie que la mémoire est susceptible de changer à tout moment et d'être modifiée, mais qu'elle ne relève pas du contrôle du programme utilisateur. Cela signifie que si vous référencez la variable, le programme devrait toujours vérifier l'adresse physique (c'est-à-dire une entrée mappée fifo) et ne pas l'utiliser de manière mise en cache.
Le Wiki dit tout sur volatile
:
Et la documentation du noyau Linux fait également une excellente notation à propos de volatile
:
En termes simples, il indique au compilateur de ne faire aucune optimisation pour une variable particulière. Les variables mappées dans le registre de périphérique sont modifiées indirectement par le périphérique. Dans ce cas, volatile doit être utilisé.
Dans le langage conçu par Dennis Ritchie, chaque accès à un objet, autre que les objets automatiques dont l'adresse n'a pas été prise, se comporterait comme s'il calculait l'adresse de l'objet puis lisait ou écrivait le stockage à cette adresse. Cela rendait le langage très puissant, mais les possibilités d’optimisation très limitées.
Bien qu’il ait été possible d’ajouter un qualificatif invitant un compilateur à supposer qu’un objet particulier ne serait pas modifié de façon étrange, une telle hypothèse serait appropriée pour la grande majorité des objets dans les programmes C, et elle aurait: Il n’était pas pratique d’ajouter un qualificatif à tous les objets pour lesquels une telle hypothèse serait appropriée. Par ailleurs, certains programmes doivent utiliser des objets pour lesquels une telle hypothèse ne serait pas valable. Pour résoudre ce problème, la norme indique que les compilateurs peuvent supposer que les objets qui ne sont pas déclarés volatile
n'auront pas leur valeur observée ou modifiée d'une manière échappant au contrôle du compilateur ou ne pouvant être comprise par un compilateur raisonnable.
Etant donné que différentes plates-formes peuvent avoir différentes manières d'observer ou de modifier des objets en dehors du contrôle d'un compilateur, il convient que les compilateurs qualité de ces plates-formes diffèrent par leur traitement exact de la sémantique volatile
. Malheureusement, la norme n’ayant pas laissé entendre que les compilateurs de qualité destinés à la programmation de bas niveau sur une plate-forme devraient traiter volatile
de manière à reconnaître tous les effets pertinents d’une opération de lecture/écriture particulière sur cette plate-forme, beaucoup les compilateurs ne parviennent pas à le faire de manière à rendre plus difficile le traitement efficace d'éléments tels que les E/S d'arrière-plan, mais ne peuvent pas être brisés par les "optimisations" du compilateur.
À mon avis, il ne faut pas trop attendre de volatile
. Pour illustrer cela, regardez l'exemple dans La réponse hautement votée de Nils Pipenbrinck .
Je dirais que son exemple ne convient pas pour volatile
. volatile
est uniquement utilisé pour: empêcher le compilateur de faire des optimisations utiles et souhaitables . Il n'y a rien sur le thread-safe, l'accès atomique ou même l'ordre de la mémoire.
Dans cet exemple:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
le gadget->data = data
avant gadget->command = command
seulement n'est garanti dans le code compilé que par le compilateur. Au moment de l'exécution, le processeur peut toujours réordonner l'affectation des données et des commandes en fonction de l'architecture du processeur. Le matériel pourrait obtenir de mauvaises données (supposons que le gadget soit mappé sur les E/S matérielles). La barrière de mémoire est nécessaire entre l’affectation des données et celle des commandes.
Une volatile peut être changée de l'extérieur du code compilé (par exemple, un programme peut mapper une variable volatile à un registre mappé en mémoire.) Le compilateur n'appliquera pas certaines optimisations au code qui gère une variable volatile - par exemple, il a gagné. t pas le charger dans un registre sans l’écrire en mémoire. Ceci est important pour les registres de matériel.