web-dev-qa-db-fra.com

Est-ce que «volatile» garantit quoi que ce soit dans le code C portable pour les systèmes multicœurs?

Après avoir regardé un tassurautrequestionsetleurréponses , j'ai l'impression que il n'y a pas d'accord général sur ce que signifie exactement le mot-clé "volatile" en C

Même la norme elle-même ne semble pas assez claire pour que tout le monde soit d'accord ce que cela signifie .

Entre autres problèmes:

  1. Il semble fournir différentes garanties en fonction de votre matériel et de votre compilateur.
  2. Cela affecte les optimisations du compilateur mais pas les optimisations matérielles, donc sur un processeur avancé qui fait ses propres optimisations au moment de l'exécution, il n'est même pas clair si le compilateur peut empêche toute optimisation que vous voulez empêcher. (Certains compilateurs génèrent des instructions pour empêcher certaines optimisations matérielles sur certains systèmes, mais cela ne semble en aucun cas normalisé.)

Pour résumer le problème, il apparaît (après avoir beaucoup lu) que "volatile" garantit quelque chose comme: La valeur sera lue/écrite non seulement depuis/vers un registre, mais au moins vers le cache L1 du cœur, dans le même ordre dans lequel les lectures/écritures apparaissent dans le code. Mais cela semble inutile, car la lecture/écriture depuis/vers un registre est déjà suffisante dans le même thread, alors que la coordination avec le cache L1 ne garantit rien en outre en ce qui concerne la coordination avec d'autres fils. Je ne peux pas imaginer quand il pourrait être important de synchroniser uniquement avec le cache L1.

UTILISEZ 1
La seule utilisation largement acceptée de volatile semble être pour les systèmes anciens ou intégrés où certains emplacements de mémoire sont mappés matériellement aux fonctions d'E/S, comme un bit en mémoire qui contrôle (directement, dans le matériel ) une lumière ou un peu en mémoire qui vous indique si une touche du clavier est enfoncée ou non (car elle est connectée directement par le matériel à la touche).

Il semble que "utiliser 1" ne se produit pas dans le code portable dont les cibles incluent les systèmes multicœurs.

UTILISATION 2
La mémoire qui peut être lue ou écrite à tout moment par un gestionnaire d'interruption (qui peut contrôler une lumière ou stocker des informations à partir d'une clé) n'est pas trop différente de "utiliser 1". Mais déjà pour cela, nous avons le problème que, selon le système, le gestionnaire d'interruption peut fonctionner surn noyau différent avec son propre cache mémoire , et "volatile" ne garantit pas la cohérence du cache sur tous les systèmes.

Donc "utiliser 2" semble être au-delà de ce que "volatile" peut apporter.

UTILISATION 3
La seule autre utilisation incontestée que je vois est d'empêcher une mauvaise optimisation des accès via différentes variables pointant vers la même mémoire que le compilateur ne réalise pas est la même mémoire. Mais cela n'est probablement pas contesté parce que les gens n'en parlent pas - je n'en ai vu qu'une mention. Et je pensais que la norme C reconnaissait déjà que des pointeurs "différents" (comme différents arguments vers une fonction) pouvaient pointer vers le même élément ou des éléments voisins, et j'ai déjà spécifié que le compilateur devait produire du code qui fonctionne même dans de tels cas. Cependant, je n'ai pas pu trouver rapidement ce sujet dans la dernière norme (500 pages!).

Donc "utiliser 3" n'existe peut-être pas du tout?

D'où ma question:

Est-ce que "volatile" garantit quelque chose dans le code C portable pour les systèmes multicœurs?


EDIT - mise à jour

Après avoir parcouru le dernière norme , il semble que la réponse soit au moins un très limité oui:
1. La norme spécifie à plusieurs reprises un traitement spécial pour le type spécifique "sig_atomic_t volatile". Cependant, la norme indique également que l'utilisation de la fonction de signal dans un programme multithread entraîne un comportement indéfini. Ce cas d'utilisation semble donc limité à la communication entre un programme monothread et son gestionnaire de signaux.
2. La norme spécifie également une signification claire pour "volatile" par rapport à setjmp/longjmp. (Un exemple de code où cela est important est donné dans d'autres questions et réponses .)

La question la plus précise devient donc:
Est-ce que "volatile" garantit quoi que ce soit dans le code C portable pour les systèmes multicœurs, à l'exception de (1) permettant à un programme à un seul thread de recevoir des informations de son gestionnaire de signaux , ou (2) permettant au code setjmp de voir les variables modifiées entre setjmp et longjmp?

C'est toujours une question oui/non.

Si "oui", ce serait bien si vous pouviez montrer un exemple de code portable sans bug qui devient bogué si "volatile" est omis. Si "non", alors je suppose qu'un compilateur est libre d'ignorer "volatile" en dehors de ces deux cas très spécifiques, pour les cibles multicœurs.

12
Matt

Pour résumer le problème, il apparaît (après avoir beaucoup lu) que "volatile" garantit quelque chose comme: La valeur sera lue/écrite non seulement depuis/vers un registre, mais au moins dans le cache L1 du noyau, dans le même ordre que les lectures/écritures apparaissent dans le code.

Non, ce n'est absolument pas le cas. Et cela rend volatile presque inutile dans le but de MT code sûr.

Si c'était le cas, alors volatile serait assez bon pour les variables partagées par plusieurs threads car la commande des événements dans le cache L1 est tout ce que vous devez faire dans un processeur typique (c'est-à-dire multicœur ou multi-processeur sur la carte mère) capable de coopérer d'une manière qui rend possible une implémentation normale de C/C++ ou Java multithreading possible avec des coûts typiques attendus (c'est-à-dire pas un coût énorme sur la plupart des opérations mutex atomiques ou non satisfaites).

Mais volatile ne fournit pas aucun ordre garanti (ou "visibilité de la mémoire") dans le cache en théorie ou en pratique.

(Remarque: ce qui suit est basé sur une bonne interprétation des documents standard, l'intention de la norme, la pratique historique et une compréhension approfondie des attentes des rédacteurs du compilateur. Cette approche basée sur l'histoire, les pratiques réelles, les attentes et la compréhension des personnes réelles dans le monde réel, qui est beaucoup plus fort et plus fiable que l'analyse des mots d'un document qui n'est pas connu pour être une écriture stellaire et qui a été révisé plusieurs fois.)

En pratique, volatile garantit ptrace-capacité qui est la capacité d'utiliser les informations de débogage pour le programme en cours d'exécution, à n'importe quel niveau d'optimisation, et le fait que les informations de débogage ont un sens pour ces objets volatils:

  • vous pouvez utiliser ptrace (un mécanisme semblable à ptrace) pour définir des points de rupture significatifs aux points de séquence après des opérations impliquant des objets volatils: vous pouvez vraiment casser exactement à ces points (notez que cela ne fonctionne que si vous êtes prêt à définir de nombreux points d'arrêt car toute instruction C/C++ peut être compilée en de nombreux points de début et de fin d'assembly différents, comme dans une boucle massivement déroulée);
  • tandis qu'un thread d'exécution s'arrête, vous pouvez lire la valeur de tous les objets volatils, car ils ont leur représentation canonique (en suivant l'ABI pour leur type respectif); une variable locale non volatile pourrait avoir une représentation atypique, f.ex. une représentation décalée: une variable utilisée pour indexer un tableau peut être multipliée par la taille des objets individuels, pour une indexation plus facile; ou il peut être remplacé par un pointeur sur un élément de tableau (tant que toutes les utilisations de la variable sont converties de manière similaire) (pensez à changer dx en du dans une intégrale);
  • vous pouvez également modifier ces objets (tant que les mappages de mémoire le permettent, car un objet volatile avec une durée de vie statique qui est qualifié const peut se trouver dans une plage de mémoire mappée en lecture seule).

La volatilité garantit en pratique un peu plus que l'interprétation stricte de ptrace: elle garantit également que les variables automatiques volatiles ont une adresse sur la pile, car elles ne sont pas affectées à un registre, une allocation de registre qui rendrait les manipulations de ptrace plus délicates (le compilateur peut sortie des informations de débogage pour expliquer comment les variables sont allouées aux registres, mais la lecture et la modification de l'état du registre sont légèrement plus complexes que l'accès aux adresses mémoire).

Notez que la capacité de débogage complète du programme, qui considère toutes les variables volatiles au moins aux points de séquence, est fournie par le mode "d'optimisation zéro" du compilateur, un mode qui effectue toujours des optimisations triviales comme des simplifications arithmétiques (il n'y a généralement pas de garantie non optimisation à tous les modes). Mais volatile est plus fort que non optimisation: x-x peut être simplifié pour un entier non volatile x mais pas pour un objet volatile.

Ainsi volatile signifie garanti d'être compilé tel quel, comme la traduction de la source en binaire/Assembly par le compilateur d'un appel système n'est pas une réinterprétation, modifiée ou optimisée de quelque façon par un compilateur. Notez que les appels de bibliothèque peuvent ou non être des appels système. De nombreuses fonctions système officielles sont en fait des fonctions de bibliothèque qui offrent une fine couche d'interposition et s'en remettent généralement au noyau à la fin. (En particulier, getpid n'a pas besoin d'aller au noyau et pourrait bien lire un emplacement mémoire fourni par le système d'exploitation contenant les informations.)

Les interactions volatiles sont des interactions avec le monde extérieur de la machine réelle, qui doit suivre la "machine abstraite". Ce ne sont pas des interactions internes des parties de programme avec d'autres parties de programme. Le compilateur ne peut que raisonner sur ce qu'il sait, à savoir les parties internes du programme.

La génération de code pour un accès volatil doit suivre l'interaction la plus naturelle avec cet emplacement mémoire: cela ne devrait pas surprendre. Cela signifie que certains accès volatils devraient être atomiques : si la façon naturelle de lire ou d'écrire la représentation d'un long sur le l'architecture est atomique, alors on s'attend à ce qu'une lecture ou une écriture d'un volatile long sera atomique, car le compilateur ne devrait pas générer de code idiot et inefficace pour accéder aux objets volatils octet par octet, par exemple .

Vous devriez pouvoir le déterminer en connaissant l'architecture. Vous n'avez rien à savoir sur le compilateur, car volatile signifie que le compilateur doit être transparent.

Mais volatile ne fait que forcer l'émission de l'Assemblée attendue pour les moins optimisés pour des cas particuliers à faire une opération mémoire: la sémantique volatile signifie la sémantique générale du cas.

Le cas général est ce que fait le compilateur lorsqu'il n'a aucune information sur une construction: f.ex. appeler une fonction virtuelle sur une valeur l via la répartition dynamique est un cas général, faire un appel direct au surchargeur après avoir déterminé au moment de la compilation le type de l'objet désigné par l'expression est un cas particulier. Le compilateur a toujours un traitement de cas général de toutes les constructions, et il suit l'ABI.

Volatile ne fait rien de spécial pour synchroniser les threads ou fournir une "visibilité mémoire": volatile ne fournit que des garanties au niveau abstrait vu de l'intérieur d'un thread en cours d'exécution ou arrêté, c'est-à-dire l'intérieur d'un coeur de CP:

  • volatile ne dit rien sur les opérations de mémoire atteignant main RAM (vous pouvez définir des types de mise en cache de mémoire spécifiques avec des instructions d'assemblage ou des appels système pour obtenir ces garanties);
  • volatile ne fournit aucune garantie quant au moment où les opérations de mémoire seront validées à n'importe quel niveau de cache (pas même L1).

Seul le deuxième point signifie que volatile n'est pas utile dans la plupart des problèmes de communication entre les threads; le premier point est essentiellement hors de propos dans tout problème de programmation qui n'implique pas de communication avec des composants matériels en dehors des CPU mais toujours sur le bus mémoire.

La propriété volatile fournissant un comportement garanti du point de vue du noyau exécutant le thread signifie que les signaux asynchrones délivrés à ce thread, qui sont exécutés du point de vue de l'ordre d'exécution de ce thread, voir les opérations dans l'ordre du code source .

Sauf si vous prévoyez d'envoyer des signaux à vos threads (une approche extrêmement utile pour la consolidation des informations sur les threads en cours d'exécution sans point d'arrêt convenu précédemment), volatile n'est pas pour vous.

1
curiousguy