Quelles règles le compilateur doit-il suivre lorsqu’il est question d’emplacements de mémoire volatile?

Je sais que lors de la lecture d’un emplacement de mémoire écrit par plusieurs threads ou processus, le mot clé volatile devrait être utilisé pour cet emplacement, comme dans certains cas ci-dessous, mais je souhaite en savoir plus sur les ressortingctions imposées au compilateur et sur les règles. le compilateur doit-il suivre lorsqu’il s’agit de ce type de cas et existe-t-il un cas exceptionnel dans lequel, malgré l’access simultané à un emplacement mémoire, le mot-clé volatile peut être ignoré par le programmeur.

volatile SomeType * ptr = someAddress; void someFunc(volatile const SomeType & input){ //function body } 

    Une optimisation particulière et très courante, exclue par volatile consiste à mettre en cache une valeur de la mémoire dans un registre et à utiliser le registre pour un access répété (car cela est beaucoup plus rapide que de revenir à la mémoire à chaque fois).

    Au lieu de cela, le compilateur doit extraire la valeur de la mémoire à chaque fois (en prenant une allusion de Zach, je devrais dire que “chaque fois” est délimité par des points de séquence).

    Une séquence d’écritures ne peut pas non plus utiliser un registre et ne réécrire que la valeur finale par la suite: chaque écriture doit être poussée en mémoire.

    Pourquoi est-ce utile? Sur certaines architectures, certains périphériques IO associent leurs entrées ou leurs sorties à un emplacement de mémoire (c’est-à-dire qu’un octet écrit à cet emplacement sort en fait de la ligne série). Si le compilateur redirige certaines de ces écritures vers un registre purgé occasionnellement, la plupart des octets ne seront pas transférés sur la ligne série. Pas bon. L’utilisation de volatile empêche cette situation.

    Ce que tu sais est faux. Volatile n’est pas utilisé pour synchroniser l’access à la mémoire entre les threads, appliquer un type quelconque de barrière de mémoire ou quelque chose du genre. Les opérations sur la mémoire volatile ne sont pas atomiques, et leur ordre ne sera pas garanti. volatile est l’un des équipements les plus mal compris de toute la langue. ” Volatile est presque inutile pour la programmation multi-thread. ”

    La fonction volatile est utilisée pour l’interfaçage avec du matériel mappé en mémoire, des gestionnaires de signaux et l’instruction de code machine setjmp .

    Il peut également être utilisé de la même manière que const , et c’est ainsi qu’Alexandrescu l’utilise dans cet article . Mais ne vous y trompez pas. volatile ne rend pas votre code magiquement thread-safe. Utilisé de cette manière spécifique, il s’agit simplement d’un outil qui peut aider le compilateur à vous indiquer où vous vous êtes trompé. Il vous appartient toujours de corriger vos erreurs, et volatile ne joue aucun rôle dans la correction de ces erreurs.

    EDIT: Je vais essayer de développer un peu sur ce que je viens de dire.

    Supposons que votre classe ait un pointeur sur quelque chose qui ne peut pas changer . Vous pouvez naturellement faire le pointeur const:

     class MyGizmo { public: const Foo* foo_; }; 

    Qu’est-ce que const vraiment pour vous ici? Cela ne fait rien pour la mémoire. Ce n’est pas comme l’onglet de protection en écriture sur une vieille disquette. La mémoire elle-même rest en écriture. Vous ne pouvez simplement pas y écrire via le pointeur foo_ . Ainsi, const est simplement un moyen de donner au compilateur un autre moyen de vous avertir lorsque vous vous trompez. Si vous deviez écrire ce code:

     gizmo.foo_->bar_ = 42; 

    … le compilateur ne le permettra pas, car il est marqué const . Évidemment, vous pouvez contourner cela en utilisant const_cast pour rejeter const -ness, mais si vous avez besoin d’être convaincu que c’est une mauvaise idée, aucune aide ne vous sera utile. 🙂

    L’utilisation d’Alexandrescu en tant que volatile est exactement la même. Cela ne fait rien pour rendre la mémoire “thread-safe” de quelque manière que ce soit . Cela permet au compilateur de vous faire savoir quand vous vous êtes trompé. Vous marquez les éléments que vous avez rendus réellement “thread-safe” (via l’utilisation d’objects de synchronisation réels, tels que des mutex ou des sémaphores) comme étant volatile . Ensuite, le compilateur ne vous laissera pas les utiliser dans un contexte non volatile . Il génère une erreur de compilation que vous devez ensuite prendre en compte et corriger. Vous pouvez à nouveau contourner le problème en const_cast la volatile aide de const_cast , mais c’est tout aussi mauvais que de rejeter const -ness.

    Mon conseil est d’abandonner complètement volatile pour écrire des applications multithread (edit 🙂 jusqu’à ce que vous sachiez vraiment ce que vous faites et pourquoi. Cela présente certains avantages, mais pas dans le sens que la plupart des gens pensent. Si vous l’utilisez mal, vous risquez d’écrire des applications dangereusement dangereuses.

    Ce n’est pas aussi bien défini que vous le souhaitez probablement. La plupart des standards pertinents de C ++ 98 se trouvent dans la section 1.9, “Exécution du programme”:

    Le comportement observable de la machine abstraite est sa séquence de lectures et d’écritures sur volatile données volatile et d’appels aux fonctions d’E / S de bibliothèque.

    Accéder à un object désigné par une valeur volatile (3.10), modifier un object, appeler une fonction d’E / S de bibliothèque ou appeler une fonction effectuant l’une de ces opérations sont tous des effets secondaires qui constituent des modifications de l’état de l’environnement d’exécution. L’évaluation d’une expression peut produire des effets secondaires. À certains points spécifiés de la séquence d’exécution, appelés points de séquence , tous les effets secondaires des évaluations précédentes doivent être complets et aucun effet secondaire des évaluations suivantes ne doit avoir eu lieu.

    Une fois que l’exécution d’une fonction commence, aucune expression de la fonction appelante n’est évaluée jusqu’à ce que l’exécution de la fonction appelée soit terminée.

    Lorsque le traitement de la machine abstraite est interrompu par la réception d’un signal, les valeurs des objects de type autre que volatile sig_atomic_t sont pas spécifiées et la valeur de tout object non volatile sig_atomic_t modifié par le gestionnaire devient indéfinie.

    Une instance de chaque object à durée de stockage automatique (3.7.2) est associée à chaque entrée de son bloc. Un tel object existe et conserve sa dernière valeur stockée pendant l’exécution du bloc et pendant que le bloc est suspendu (par un appel d’une fonction ou la réception d’un signal).

    Les exigences minimales pour une implémentation conforme sont:

    • Aux points de la séquence, volatile objects volatile sont stables, en ce sens que les évaluations précédentes sont terminées et que les évaluations suivantes ne se sont pas encore produites.

    • À la fin du programme, toutes les données écrites dans les fichiers doivent être identiques à l’un des résultats possibles que l’exécution du programme aurait obtenus conformément à la sémantique abstraite.

    • La dynamic d’entrée et de sortie des dispositifs interactifs doit s’effectuer de manière à ce que des messages incitatifs apparaissent avant qu’un programme n’attende une entrée. Ce qui constitue un dispositif interactif est défini par la mise en œuvre.

    Donc, ce qui revient à est:

    • Le compilateur ne peut pas optimiser les lectures ou écritures sur volatile objects volatile . Pour des cas simples comme celui de Casablanca mentionné, cela fonctionne comme vous pourriez le penser. Cependant, dans des cas comme

       volatile int a; int b; b = a = 42; 

      les gens peuvent et discutent pour savoir si le compilateur doit générer du code comme si la dernière ligne avait été lue

       a = 42; b = a; 

      ou s’il peut, comme il le ferait normalement (en l’absence de volatile ), générer

       a = 42; b = 42; 

      (C ++ 0x a peut-être résolu ce problème, je n’ai pas tout lu.)

    • Le compilateur ne peut pas réorganiser les opérations sur deux objects volatile différents qui se produisent dans des instructions distinctes (chaque point-virgule est un sharepoint séquence), mais il est totalement autorisé de réorganiser les access aux objects non volatils par rapport aux objects volatils. C’est l’une des nombreuses raisons pour lesquelles vous ne devriez pas essayer d’écrire vos propres spinlocks et c’est la raison principale pour laquelle John Dibling vous avertit de ne pas traiter les éléments volatile comme une panacée pour les programmes multithread.

    • En parlant de discussions, vous aurez remarqué l’ absence totale de mention de discussions dans le texte des normes. C’est parce que C ++ 98 n’a pas de concept de thread . (C ++ 0x le fait, et peut bien spécifier leur interaction avec volatile , mais je ne présumerais même pas que quelqu’un appliquera ces règles si j’étais vous-même.) Par conséquent, rien ne garantit que les access aux objects volatile d’un seul thread sont visibles. à un autre fil. C’est l’autre raison majeure pour laquelle volatile n’est pas particulièrement utile pour la programmation multithread.

    • Rien ne garantit que volatile objects volatile sont accessibles en une seule fois ou que les modifications apscopes aux objects volatile évitent de toucher d’autres éléments en mémoire. Ce n’est pas explicite dans ce que j’ai cité, mais c’est ce volatile sig_atomic_t les sig_atomic_t sur la volatile sig_atomic_t – la partie sig_atomic_t ne serait pas nécessaire autrement. Cela rend volatile composants volatile considérablement moins utiles pour l’access aux périphériques d’E / S que prévu, et les compilateurs commercialisés pour la programmation intégrée offrent souvent des garanties plus solides, mais ce n’est pas une chose sur laquelle vous pouvez compter.

    • Beaucoup de gens essaient de créer des access spécifiques à des objects ayant volatile sémantique volatile ,

       T x; *(volatile T *)&x = foo(); 

      Ceci est légitime (car il est dit “object désigné par une valeur volatile ” et non pas “object de type volatile “) mais doit être fait avec beaucoup de soin, car rappelez-vous ce que j’ai dit à propos du compilateur étant totalement autorisé à réordonner les fonctions non volatiles. access par rapport aux volatiles? Cela va même si c’est le même object (autant que je sache de toute façon).

    • Si vous souhaitez réorganiser les access à plus d’une valeur volatile, vous devez comprendre les règles de sharepoint séquence , qui sont longues et compliquées. Je ne vais pas les citer ici car cette réponse est déjà trop longue. bonne explication qui n’est qu’un peu simplifiée . Si vous avez besoin de vous soucier des différences entre les règles de sharepoint séquence entre C et C ++, vous vous êtes déjà trompé (par exemple, en règle générale, ne surchargez jamais && ).

    Déclarer une variable comme volatile signifie que le compilateur ne peut faire aucune hypothèse sur la valeur qu’il aurait pu obtenir autrement, et empêche donc le compilateur d’appliquer diverses optimisations. Cela oblige essentiellement le compilateur à relire la valeur de la mémoire à chaque access, même si le stream de code normal ne modifie pas la valeur. Par exemple:

     int *i = ...; cout << *i; // line A // ... (some code that doesn't use i) cout << *i; // line B 

    Dans ce cas, le compilateur suppose normalement que, puisque la valeur en i n’a pas été modifiée entre les deux, il est correct de conserver la valeur de la ligne A (par exemple dans un registre) et d’afficher la même valeur dans B. Cependant, si vous cochez aussi volatile , vous dites au compilateur que certaines sources externes pourraient éventuellement modifier la valeur entre i lignes A et B. Le compilateur doit donc extraire à nouveau la valeur actuelle de la mémoire.

    Le compilateur n’est pas autorisé à optimiser les lectures éloignées d’un object volatil dans une boucle, ce qui serait normalement le cas (par exemple, strlen ()).

    Il est couramment utilisé dans la programmation intégrée lors de la lecture d’un registre de matériel à une adresse fixe et cette valeur peut changer de manière inattendue. (Contrairement à la mémoire “normale”, cela ne change pas sauf si écrit par le programme lui-même …)

    C’est son but principal.

    Il pourrait également être utilisé pour s’assurer qu’un thread voit le changement d’une valeur écrite par un autre, mais cela ne garantit en aucun cas l’atomicité lors de la lecture / écriture sur ledit object.