Pourquoi le volatile est-il nécessaire en C?

Pourquoi le volatile est-il nécessaire en C? A quoi cela sert? Que va-t-il faire?

Volatile dit au compilateur de ne pas optimiser tout ce qui concerne la variable volatile.

Il n’ya qu’une seule raison de l’utiliser: lorsque vous utilisez une interface avec du matériel.

Disons que vous avez un petit morceau de matériel qui est mappé dans la RAM quelque part 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; } 

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 coup d’oeil à la boucle d’attente pendant que occupé. Celui-ci sera optimisé. Le compilateur essaiera d’être intelligent, de lire la valeur d’isbusy une seule fois et de passer à une boucle infinie. Ce n’est pas ce que vous voulez.

La manière de contourner cela 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 modifier l’ordre des affectations:

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; } 

Une autre utilisation de volatile est le traitement des signaux. Si vous avez un code comme celui-ci:

 quit = 0; while (!quit) { /* very small loop which is completely visible to the comstackr */ } 

Le compilateur est autorisé à remarquer que le corps de la boucle ne touche pas la variable quit et à convertir la boucle en 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 le charger à chaque fois, car il peut être modifié ailleurs. C’est exactement ce que vous voulez dans cette situation.

volatile dans C a réellement été créé dans le but de ne pas mettre en cache automatiquement les valeurs de la variable. Cela indiquera à la machine de ne pas mettre en cache la valeur de cette variable. Ainsi, la valeur de la variable volatile donnée sera prise en compte dans la mémoire principale chaque fois qu’elle le rencontrera. Ce mécanisme est utilisé car à tout moment, la valeur peut être modifiée par le système d’exploitation ou toute interruption. Donc, l’utilisation de volatile nous aidera à accéder à la valeur à chaque fois.

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é I / O. Si cela n’est pas spécifié dans de tels cas, certains access aux 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 d’Andrei Alexandrescu, ” volatile – 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é à le mettre en cache dans un registre – une optimisation commune qui serait désastreuse si cette variable était partagée entre plusieurs threads. Donc, la règle générale est la suivante: si vous avez des variables de type primitif qui doivent être partagées entre plusieurs threads, déclarez ces variables volatiles . Mais vous pouvez réellement 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 du code.

L’article s’applique à la fois à C et à C++ .

Voir aussi l’article ” C ++ et les périls du locking à double contrôle ” par Scott Meyers et Andrei Alexandrescu:

Ainsi, pour certains emplacements de mémoire (par exemple, les ports mappés en mémoire ou la mémoire référencée par les routeurs 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 volatiles sont “observables” doit être exécuté 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 de manière informelle que les garanties volatiles de C et C ++.

Ma simple explication est la suivante:

Dans certains scénarios, basés sur la logique ou le code, le compilateur optimisera les variables qui, selon lui, ne changent pas. Le mot clé volatile 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 } 

À 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 optimisation, le compilateur le traitera tout comme while(true) tout le temps, ce qui donnera 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 du programme, c’est-à-dire, ne l’optimisez pas. C’est le cas d’utilisation de volatile.

Une utilisation marginale pour volatile est la suivante. Disons que vous voulez 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+hx n’est généralement pas égal à h raison d’erreurs d’arrondi. Pensez-y: lorsque vous soustrayez des nombres très proches, vous perdez beaucoup de chiffres significatifs, ce qui peut 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. Donc, vous écrivez à la place

  volatile double hh = x + h; hh -= x; 

pour forcer le compilateur à lire l’emplacement mémoire contenant hh, renoncer à une éventuelle opportunité d’optimisation.

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

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

  2. La volatilité est utilisée pour accéder à des emplacements de mémoire exacts dans la mémoire vive, la mémoire morte, la mémoire morte, etc. Elle est utilisée 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 les exemples avec liste de assembly. 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 volatiles sont importants.

Supposons que vous mappiez un fichier pour une E / S plus rapide et que ce fichier puisse changer en arrière-plan (par exemple, le fichier ne se trouve pas sur votre disque dur local, mais sur un réseau).

Si vous accédez aux données du fichier mappé en mémoire via des pointeurs vers des objects non volatiles (au niveau du code source), le code généré par le compilateur peut récupérer les mêmes données plusieurs fois sans que vous en ayez conscience.

Si ces données changent, votre programme peut utiliser deux versions différentes ou plus des données et entrer 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 ou des fichiers non fiables provenant d’emplacements non fiables.

Si vous vous souciez de la sécurité, et que vous devriez, c’est un scénario important à prendre en compte.

volatile signifie que le stockage est susceptible de changer à tout moment et d’être modifié mais quelque chose en dehors du 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.-à-d. Un fifo d’entrée mappé), et ne pas l’utiliser de manière mise en cache.

Le wiki dit tout sur les volatile :

  • volatile (programmation informatique)

Et le doc du kernel Linux fait également une excellente notation à propos de volatile :

  • Pourquoi la classe de type “volatile” ne doit pas être utilisée

Un volatile peut être changé en dehors 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 en mémoire. Ceci est important lorsque vous traitez des registres matériels.

À mon avis, vous ne devriez pas vous attendre à trop de volatile . Pour illustrer cela, regardez l’exemple de 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 d’effectuer des optimisations utiles et souhaitables . Il n’y a rien sur la sécurité des threads, l’access 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 before gadget->command = command only est uniquement garanti dans le code compilé par le compilateur. Au moment de l’exécution, le processeur peut encore réorganiser les données et l’affectation des commandes, en fonction de l’architecture du processeur. Le matériel pourrait recevoir des données erronées (supposez que le gadget soit mappé sur les E / S matérielles). La barrière de mémoire est nécessaire entre les données et l’affectation des commandes.

Dans le langage conçu par Dennis Ritchie, tout access à un object, autre que les objects automatiques dont l’adresse n’a pas été prise, se comporterait comme s’il calculait l’adresse de l’object, puis lisait ou écrivait le stockage à cette adresse. Cela a rendu le langage très puissant, mais les possibilités d’optimisation étaient extrêmement limitées.

Bien qu’il ait été possible d’append un qualificatif qui inviterait un compilateur à supposer qu’un object particulier ne serait pas modifié de manière étrange, une telle hypothèse serait appropriée pour la grande majorité des objects des programmes C, et n’a pas été pratique d’append un qualificatif à tous les objects pour lesquels une telle hypothèse serait appropriée. D’autre part, certains programmes doivent utiliser des objects 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 objects qui ne sont pas déclarés volatile n’auront pas leur valeur observée ou modifiée de manière hors du contrôle du compilateur, ou hors de la compréhension d’un compilateur raisonnable.

Étant donné que les objects peuvent être observés ou modifiés de différentes manières par des plates-formes différentes en dehors du contrôle du compilateur, il est approprié que les compilateurs de qualité pour ces plates-formes diffèrent dans leur traitement exact de la sémantique volatile . Malheureusement, parce que la norme n’a pas réussi à suggérer que les compilateurs de qualité destinés à une programmation de bas niveau sur une plate-forme doivent traiter de manière volatile en reconnaissant tous les effets d’une opération de lecture / écriture sur cette plate-forme Ce faisant, il est plus difficile de traiter des choses comme des E / S d’arrière-plan d’une manière efficace, mais qui ne peut pas être interrompue par les “optimisations” du compilateur.

Il ne permet pas au compilateur de modifier automatiquement les valeurs des variables. une variable volatile est pour une utilisation dynamic.