Pourquoi est-il volatile
nécessaire en C? A quoi cela sert? Que va-t-il faire?
Pourquoi est-il volatile
nécessaire en C? A quoi cela sert? Que va-t-il faire?
Réponses:
Volatile indique au compilateur de ne pas optimiser tout ce qui a à voir avec 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 à partir du code visible: lorsque vous vous connectez avec du matériel qui modifie la valeur elle-même; quand il y a un autre thread en cours d'exécution qui utilise également la variable; ou quand il y a un gestionnaire de signal qui pourrait changer la valeur de la variable.
Disons que vous avez un petit matériel qui est mappé quelque part en RAM et qui a 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 il peut échouer car le compilateur est libre de modifier 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 œil à la boucle d'attente pendant l'occupation. Celui-là sera optimisé. Le compilateur essaiera d'être intelligent, ne lira la valeur d'isbusy qu'une seule fois, puis entrera dans une boucle infinie. Ce n'est pas ce que tu veux.
Le moyen de contourner ce problème consiste à déclarer le gadget de pointeur comme volatile. De cette façon, le compilateur est obligé de faire ce que vous avez écrit. Il ne peut pas supprimer les affectations de mémoire, il ne peut pas mettre en cache les variables dans les registres et il ne peut pas non plus changer l'ordre des affectations:
Ceci est la bonne version:
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
en C a en fait vu le jour dans le but de ne pas mettre automatiquement en cache les valeurs de la variable. Il indiquera au compilateur de ne pas mettre en cache la valeur de cette variable. Il va donc générer du code pour prendre la valeur de la volatile
variable donnée de la mémoire principale à chaque fois qu'il la rencontre. Ce mécanisme est utilisé car à tout moment la valeur peut être modifiée par l'OS ou n'importe quelle interruption. Donc, l'utilisation volatile
nous aidera à accéder à chaque fois à la valeur.
volatile
était de permettre aux compilateurs d'optimiser le code tout en permettant aux programmeurs de réaliser la sémantique qui serait obtenue sans de telles optimisations. Les auteurs de la norme s'attendaient à ce que les implémentations de qualité prennent en charge toutes les sémantiques utiles compte tenu de leurs plates-formes cibles et de leurs champs d'application, et ne s'attendaient pas à ce que les rédacteurs de compilateurs cherchent à offrir la sémantique de qualité la plus basse conforme à la norme et n'étaient pas à 100%. stupide (notez que les auteurs de la norme reconnaissent explicitement dans la justification ...
Les volatile
gestionnaires de signaux sont également utilisés . 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 quit
variable et convertit la boucle en while (true)
boucle. Même si la quit
variable est définie sur le gestionnaire de signaux pour SIGINT
etSIGTERM
; le compilateur n'a aucun moyen de le savoir.
Cependant, si la quit
variable 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.
quit
, le compilateur peut l'optimiser en une boucle constante, en supposant qu'il n'y a aucun moyen de quit
changer entre les itérations. NB: Ce n'est pas nécessairement un bon substitut à une programmation threadsafe réelle.
volatile
ou d'autres marqueurs, il supposera que rien en dehors de la boucle ne modifie cette variable une fois qu'elle entre dans la boucle, même s'il s'agit d'une variable globale.
extern int global; void fn(void) { while (global != 0) { } }
avec gcc -O3 -S
et regardez le fichier d'assemblage résultant, sur ma machine c'est le cas movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
, c'est-à-dire une boucle infinie si le global n'est pas nul. Essayez ensuite d'ajouter volatile
et de voir la différence.
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é d'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 mémoire ne peut pas être relu.
Voir cet article d'Andrei Alexandrescu, " volatile - le meilleur ami du programmeur multithread "
Le mot clé volatile a été conçu pour empêcher les optimisations du compilateur qui pourraient rendre le code incorrect en présence de certains événements asynchrones. Par exemple, si vous déclarez une variable primitive comme volatile , le compilateur n'est pas autorisé à la mettre en cache dans un registre - une optimisation courante qui serait désastreuse si cette variable était partagée entre plusieurs threads. Donc, la règle générale est, si vous avez des variables de type primitif qui doivent être partagées entre plusieurs threads, déclarez ces variables volatiles. Mais vous pouvez en fait faire beaucoup plus avec ce mot-clé: vous pouvez l'utiliser pour intercepter du code qui n'est pas sûr pour les threads, et vous pouvez le faire au moment de la compilation. Cet article montre comment cela se fait; la solution implique un simple pointeur intelligent qui facilite également la sérialisation des sections critiques de code.
L'article s'applique à la fois à C
et C++
.
Voir également l'article " C ++ et les dangers du verrouillage à double vérification " de Scott Meyers et Andrei Alexandrescu:
Ainsi, lorsque vous traitez avec certains emplacements de mémoire (par exemple, les ports mappés en mémoire ou la mémoire référencée par les ISR [Interrupt Service Routines]), certaines optimisations doivent être suspendues. volatile existe pour spécifier un traitement spécial pour de tels emplacements, en particulier: (1) le contenu d'une variable volatile est "instable" (peut changer par des moyens inconnus du compilateur), (2) toutes les écritures sur des données volatiles sont "observables" afin qu'elles doit être exécuté religieusement, et (3) toutes les opérations sur les 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 qui mélangent entrée et sortie. C'est officieusement ce que les garanties volatiles de C et C ++.
volatile
ne garantit pas l'atomicité.
Ma simple explication est:
Dans certains scénarios, basé sur la logique ou le code, le compilateur optimisera les variables qu'il pense ne pas changer. Le volatile
mot-clé empêche l'optimisation d'une variable.
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 qu'il usb_interface_flag
est défini comme 0, et que dans la boucle while, il sera à jamais nul. Après l'optimisation, le compilateur le traitera comme while(true)
tout le temps, résultant en une boucle infinie.
Pour éviter ce genre de scénarios, nous déclarons l'indicateur comme volatile, 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 pour volatile.
Une utilisation marginale de volatile est la suivante. Supposons que vous souhaitiez 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 ce x+h-x
n'est généralement pas égal à en h
raison d'erreurs d'arrondi. Pensez-y: lorsque vous soustrayez des nombres très proches, vous perdez beaucoup de chiffres significatifs qui peuvent ruiner 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 en fonction de votre plate-forme et des commutateurs du compilateur, la deuxième ligne de cette fonction peut être effacée par un compilateur optimisant de manière agressive. Alors tu écris à la place
volatile double hh = x + h;
hh -= x;
pour forcer le compilateur à lire l'emplacement de mémoire contenant hh, perdant ainsi une éventuelle opportunité d'optimisation.
h
ou la hh
formule dérivée? Lorsque hh
est calculé, la dernière formule l'utilise comme la première, sans différence. Peut-être que ça devrait l'être (f(x+h) - f(x))/hh
?
h
et hh
est qu'elle hh
est tronquée à une puissance négative de deux par l'opération x + h - x
. Dans ce cas, x + hh
et x
diffèrent exactement par hh
. Vous pouvez aussi prendre votre formule, elle donnera le même résultat, puisque x + h
et x + hh
sont égales (c'est le dénominateur qui est important ici).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? sans utiliser le volatile.
-ffast-math
ou équivalent.
Il y a deux utilisations. Ceux-ci sont spécialement utilisés plus souvent dans le développement embarqué.
Le compilateur n'optimisera pas les fonctions qui utilisent des variables définies avec un mot clé volatile
Volatile est utilisé pour accéder aux emplacements de mémoire exacts dans la RAM, la ROM, etc. Il est utilisé plus souvent pour contrôler les périphériques mappés en mémoire, accéder aux registres du processeur et localiser des emplacements de mémoire spécifiques.
Voir des exemples avec la liste des assemblages. Re: Utilisation du mot-clé C «volatile» 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-benchmark).
Je mentionnerai un autre scénario où les volatils sont importants.
Supposons que vous mappiez en mémoire un fichier pour des E / S plus rapides et que ce fichier puisse changer 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 vers 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, votre programme peut utiliser deux ou plusieurs versions différentes des données et entrer dans un état incohérent. Cela peut conduire non seulement à un comportement logiquement incorrect du programme, mais également à des failles de sécurité exploitables dans celui-ci s'il traite des fichiers non fiables ou des fichiers provenant d'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 le stockage est susceptible de changer à tout moment et d'être modifié, mais quelque chose échappant au contrôle du programme utilisateur. Cela signifie que si vous référencez la variable, le programme doit 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 doc du noyau Linux fait également une excellente notation sur volatile
:
À mon avis, il ne faut pas trop en attendre volatile
. Pour illustrer cela, regardez l'exemple de la réponse très appréciée de Nils Pipenbrinck .
Je dirais que son exemple ne convient pas volatile
. volatile
est uniquement utilisé pour:
empêcher le compilateur d'effectuer des optimisations utiles et souhaitables . Il ne s'agit pas du thread safe, de l'accès atomique ou même de 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;
}
L' gadget->data = data
avant gadget->command = command
seulement n'est garanti que dans le code compilé par le compilateur. Au moment de l'exécution, le processeur réordonne peut-être encore l'affectation des données et des commandes, en fonction de l'architecture du processeur. Le matériel peut obtenir des données incorrectes (supposons que le gadget soit mappé sur les E / S matérielles). La barrière mémoire est nécessaire entre les données et l'affectation des commandes.
volatile
dégrader les performances sans raison. Quant à savoir s'il est suffisant, cela dépendra d'autres aspects du système que le programmeur peut en savoir plus que le compilateur. D'un autre côté, si un processeur garantit qu'une instruction d'écrire à une certaine adresse videra le cache du CPU mais qu'un compilateur ne fournira aucun moyen de vider les variables mises en cache de registre dont le CPU ne sait rien, vider le cache serait inutile.
Dans le langage conçu par Dennis Ritchie, chaque accès à un objet, autre que des objets automatiques dont l'adresse n'avait pas été prise, se comporterait comme s'il calculait l'adresse de l'objet, puis lisait ou écrivait le stockage à cette adresse. Cela a rendu le langage très puissant, mais les possibilités d'optimisation étaient très limitées.
Bien qu'il aurait pu être possible d'ajouter un qualificatif qui inviterait un compilateur à supposer qu'un objet particulier ne serait pas modifié de manière étrange, une telle hypothèse serait appropriée pour la grande majorité des objets dans les programmes C, et elle aurait été impossible d’ajouter un qualificatif à tous les objets pour lesquels une telle hypothèse serait appropriée. D'un autre côté, certains programmes doivent utiliser des objets pour lesquels une telle hypothèse ne serait pas vérifiée. 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 qui échappe au contrôle du compilateur, ou serait en dehors de la compréhension d'un compilateur raisonnable.
Étant donné que différentes plates-formes peuvent avoir des façons différentes d'observer ou de modifier des objets en dehors du contrôle d'un compilateur, il est approprié que les compilateurs de qualité pour ces plates-formes diffèrent dans leur traitement exact de la volatile
sémantique. Malheureusement, parce que la norme n'a pas suggéré que les compilateurs de qualité destinés à la programmation de bas niveau sur une plate-forme devraient gérer volatile
d'une manière qui reconnaîtrait tous les effets pertinents d'une opération de lecture / écriture particulière sur cette plate-forme, de nombreux compilateurs sont loin de le faire. donc d'une manière qui rend plus difficile le traitement de choses comme les E / S en arrière-plan d'une manière efficace mais qui ne peut pas être interrompue par les "optimisations" du compilateur.
En termes simples, il indique au compilateur de ne faire aucune optimisation sur une variable particulière. Les variables qui sont mappées au registre de périphérique sont modifiées indirectement par le périphérique. Dans ce cas, volatile doit être utilisé.
Un volatile peut être modifié depuis 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 le charger dans un registre sans l'écrire dans la mémoire. Ceci est important lorsqu'il s'agit de registres matériels.
Comme beaucoup le suggèrent à juste titre ici, l'utilisation populaire du mot-clé volatile consiste à ignorer l'optimisation de la variable volatile.
Le meilleur avantage qui vient à l'esprit, et qui mérite d'être mentionné après avoir lu à propos de volatile, c'est - pour éviter le retour en arrière de la variable en cas delongjmp
. Un saut non local.
Qu'est-ce que ça veut dire?
Cela signifie simplement que la dernière valeur sera conservée après avoir déroulé la pile , pour revenir à une trame de pile précédente; généralement en cas de scénario erroné.
Puisqu'il serait hors de portée de cette question, je ne vais pas entrer dans les détails de setjmp/longjmp
ici, mais cela vaut la peine d'être lu à ce sujet; et comment la fonction de volatilité peut être utilisée pour conserver la dernière valeur.