Pourquoi la volatilité est-elle nécessaire en C?

Réponses:

425

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;
    }
Nils Pipenbrinck
la source
46
Personnellement, je préférerais que la taille entière soit explicite, par exemple int8 / int16 / int32 lors de la conversation avec le matériel.
Bonne
22
oui, vous devez déclarer des choses avec une taille de registre fixe, mais bon - ce n'est qu'un exemple.
Nils Pipenbrinck
69
Volatile est également nécessaire dans le code threadé lorsque vous jouez avec des données qui ne sont pas protégées par concurrence. Et oui, il y a des moments valables pour le faire, vous pouvez par exemple écrire une file d'attente de messages circulaire sécurisée pour les threads sans avoir besoin d'une protection de concurrence explicite, mais cela aura besoin de volatils.
Gordon Wrigley
14
Lisez plus attentivement la spécification C. Volatile n'a un comportement défini que sur les E / S des périphériques mappés en mémoire ou sur la mémoire touchée par une fonction d'interruption asynchrone. Il ne dit rien sur le threading, et un compilateur qui optimise l'accès distant à la mémoire touchée par plusieurs threads est conforme.
éphémère
17
@tolomea: complètement faux. triste 17 personnes ne le savent pas. volatile n'est pas une barrière de mémoire. elle est uniquement liée à l' évitement de l'élision du code lors de l'optimisation basée sur l'hypothèse d'effets secondaires non visibles .
v.oddou
188

volatileen 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 volatilevariable 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 volatilenous aidera à accéder à chaque fois à la valeur.

Manoj Doubts
la source
Est venu au monde? Le «volatile» n'était-il pas à l'origine emprunté au C ++? Eh bien, il me semble que je me souviens ...
syntaxerror
Ce n'est pas du tout volatile - cela interdit également une réorganisation si spécifié comme volatile.
FaceBro
4
@FaceBro: Le but de 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 ...
supercat
1
... qu'il est possible qu'une implémentation soit conforme sans être de bonne qualité pour convenir à n'importe quel but, mais ils n'ont pas jugé nécessaire d'empêcher cela).
supercat
1
@syntaxerror comment peut-il être emprunté à C ++ alors que C avait plus d'une décennie de plus que C ++ (à la fois sur les premières versions et les premières normes)?
phuclv
178

Les volatilegestionnaires 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 quitvariable et convertit la boucle en while (true)boucle. Même si la quitvariable est définie sur le gestionnaire de signaux pour SIGINTetSIGTERM ; le compilateur n'a aucun moyen de le savoir.

Cependant, si la quitvariable 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.

CesarB
la source
quand vous dites "le compilateur est forcé de le charger à chaque fois, est-ce comme quand le compilateur décide d'optimiser une certaine variable et que nous ne déclarons pas la variable comme volatile, au moment de l'exécution, certaines variables sont chargées dans des registres CPU non en mémoire ?
Amit Singh Tomar
1
@AmitSinghTomar Cela signifie ce qu'il dit: Chaque fois que le code vérifie la valeur, il est rechargé. Sinon, le compilateur est autorisé à supposer que les fonctions qui ne prennent pas de référence à la variable ne peuvent pas la modifier, donc en supposant que CesarB voulait que la boucle ci-dessus ne soit pas définie quit, le compilateur peut l'optimiser en une boucle constante, en supposant qu'il n'y a aucun moyen de quitchanger entre les itérations. NB: Ce n'est pas nécessairement un bon substitut à une programmation threadsafe réelle.
underscore_d
si quit est une variable globale, le compilateur ne doit pas optimiser la boucle while, n'est-ce pas?
Pierre G.20
2
@PierreG. Non, le compilateur peut toujours supposer que le code est monothread, sauf indication contraire. Autrement dit, en l'absence de volatileou 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.
CesarB
1
@PierreG. Oui, essayez par exemple de compiler extern int global; void fn(void) { while (global != 0) { } }avec gcc -O3 -Set 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 volatileet de voir la différence.
CesarB du
60

volatileindique 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.

Chris Jester-Young
la source
30

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 à Cet 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 ++.

Robert S. Barnes
la source
La norme spécifie-t-elle si une lecture est considérée comme un «comportement observable» si la valeur n'est jamais utilisée? Mon impression est que ça devrait être le cas, mais quand j'ai prétendu que c'était ailleurs, quelqu'un m'a mis au défi pour une citation. Il me semble que sur toute plate-forme où la lecture d'une variable volatile pourrait avoir un effet, un compilateur devrait être obligé de générer du code qui effectue chaque lecture indiquée précisément une fois; sans cette exigence, il serait difficile d'écrire du code qui génère une séquence prévisible de lectures.
supercat
@supercat: Selon le premier article, "Si vous utilisez le modificateur volatile sur une variable, le compilateur ne mettra pas cette variable en cache dans les registres - chaque accès atteindra l'emplacement de mémoire réel de cette variable." En outre, dans la section §6.7.3.6 de la norme c99, il est dit: "Un objet qui a un type qualifié de volatile peut être modifié d'une manière inconnue de l'implémentation ou avoir d'autres effets secondaires inconnus." Cela implique en outre que les variables volatiles ne peuvent pas être mises en cache dans les registres et que toutes les lectures et écritures doivent être exécutées dans l'ordre par rapport aux points de séquence, qu'elles sont en fait observables.
Robert S.Barnes
Ce dernier article déclare en effet explicitement que les lectures sont des effets secondaires. Le premier indique que les lectures ne peuvent pas être effectuées hors séquence, mais ne semble pas exclure la possibilité qu’elles soient complètement élidées.
supercat
"le compilateur n'est pas autorisé à le mettre en cache dans un registre" - La plupart des architectures RISC sont des machines à registre, donc toute lecture-modification-écriture doit mettre en cache l'objet dans un registre. volatilene garantit pas l'atomicité.
trop honnête pour ce site
1
@Olaf: Charger quelque chose dans un registre n'est pas la même chose que la mise en cache. La mise en cache affecterait le nombre de charges ou de magasins ou leur calendrier.
supercat
28

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 volatilemot-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_flagest 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.

Venkatakrishna Kalepalli
la source
19

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-xn'est généralement pas égal à en hraison 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.

Alexandre C.
la source
Quelle est la différence entre l'utilisation hou la hhformule dérivée? Lorsque hhest 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?
Sergey Zhukov
2
La différence entre het hhest qu'elle hhest tronquée à une puissance négative de deux par l'opération x + h - x. Dans ce cas, x + hhet xdiffèrent exactement par hh. Vous pouvez aussi prendre votre formule, elle donnera le même résultat, puisque x + het x + hhsont égales (c'est le dénominateur qui est important ici).
Alexandre C.
3
N'est-ce pas une façon plus lisible d'écrire cela x1=x+h; d = (f(x1)-f(x))/(x1-x)? sans utiliser le volatile.
Sergey Zhukov
Une référence qu'un compilateur peut effacer cette deuxième ligne de la fonction?
CoffeeTableEspresso
@CoffeeTableEspresso: Non, désolé. Plus j'en sais sur la virgule flottante, plus je crois que le compilateur n'est autorisé à l'optimiser que s'il le dit explicitement, avec -ffast-mathou équivalent.
Alexandre C.
11

Il y a deux utilisations. Ceux-ci sont spécialement utilisés plus souvent dans le développement embarqué.

  1. Le compilateur n'optimisera pas les fonctions qui utilisent des variables définies avec un mot clé volatile

  2. 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é

Neo Cambell
la source
"Le compilateur n'optimisera pas les fonctions qui utilisent des variables définies avec un mot clé volatile" - c'est tout à fait faux.
trop honnête pour ce site
10

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).

Diomidis Spinellis
la source
10

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.

Alexey Frunze
la source
7

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.

Rembourrage de structure
la source
Aucun compilateur ne prend volatile pour signifier "adresse physique dans la RAM" ou "contourner le cache".
curiousguy
5

À 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. volatileest 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 = dataavant gadget->command = commandseulement 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.

Oliver
la source
2
Je dirais que volatile est utilisé pour empêcher le compilateur de faire des optimisations qui seraient normalement utiles et souhaitables. Tel qu'il est écrit, cela semble volatiledé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.
supercat
5

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 volatilen'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 volatilesé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 volatiled'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.

supercat
la source
5

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é.

Rajeshsam
la source
1
Y a-t-il quelque chose de nouveau dans cette réponse qui n'a pas été mentionné auparavant?
slfan
3

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.

Ori Pessach
la source
0

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.

Siddharth
la source
-2

il ne permet pas au compilateur de changer automatiquement les valeurs des variables. une variable volatile est à usage dynamique.

venu
la source