web-dev-qa-db-fra.com

Est-il permis à un compilateur d’optimiser une variable volatile locale?

Le compilateur est-il autorisé à optimiser ceci (selon le standard C++ 17):

int fn() {
    volatile int x = 0;
    return x;
}

pour ça?

int fn() {
    return 0;
}

Si oui pourquoi? Si non pourquoi pas


Voici quelques réflexions à ce sujet: les compilateurs actuels compilent fn() sous forme de variable locale placée sur la pile, puis la retournent. Par exemple, sur les x86-64, gcc crée ceci:

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret    

Pour autant que je sache, la norme ne dit pas qu’une variable volatile locale doit être mise sur la pile. Donc, cette version serait tout aussi bonne:

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret    

Ici, edx stocke x. Mais maintenant, pourquoi s'arrêter ici? Comme edx et eax sont tous deux égaux à zéro, nous pourrions simplement dire:

xor    eax,eax // eax is the return, and x as well
ret    

Et nous avons transformé fn() vers la version optimisée. Cette transformation est-elle valide? Si non, quelle étape est invalide?

72
geza

Non. L'accès à volatile objets est considéré comme un comportement observable, exactement comme les E/S, sans distinction particulière entre les variables locales et globales.

Les exigences minimales pour une implémentation conforme sont:

  • L'accès aux objets volatile est évalué strictement selon les règles de la machine abstraite.

[...]

Celles-ci sont collectivement appelées le comportement observable du programme.

N3690, [intro.execution], ¶8

Comment ce qui est observé est en dehors du domaine d'application de la norme et tombe directement dans le territoire spécifique à l'implémentation, exactement comme les entrées/sorties et l'accès aux objets globaux volatile. volatile signifie "vous pensez que vous savez tout ce qui se passe ici, mais ce n'est pas comme ça; faites-moi confiance et faites ce travail sans être trop intelligent, car je suis dans votre programme en train de faire mon travail secret avec vos octets". Ceci est en fait expliqué à [dcl.type.cv] ¶7:

[Remarque: volatile est un indice pour l'implémentation afin d'éviter une optimisation agressive impliquant l'objet car la valeur de l'objet peut être modifiée de manière indétectable par une implémentation. De plus, pour certaines implémentations, volatile peut indiquer que des instructions matérielles spéciales sont nécessaires pour accéder à l'objet. Voir 1.9 pour la sémantique détaillée. En général, la sémantique de volatile est censée être la même en C++ qu’en C. - end note]

59
Matteo Italia

Cette boucle peut être optimisée par la règle as-if car elle n'a pas de comportement observable:

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

Celui-ci ne peut pas:

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

La deuxième boucle fait quelque chose à chaque itération, ce qui signifie que la boucle prend O(n) temps. Je n'ai aucune idée de ce qu'est la constante, mais je peux la mesurer, puis j'ai une façon de faire une boucle pendant un temps (plus ou moins) connu.

Je peux le faire parce que la norme stipule que l’accès aux produits volatils doit avoir lieu, dans l’ordre. Si un compilateur décidait que dans ce cas la norme ne s’appliquait pas, j’aurais le droit de déposer un rapport de bogue.

Si le compilateur choisit de mettre looped dans un registre, je suppose que je n'ai aucun argument valable contre cela. Mais il doit toujours définir la valeur de ce registre sur 1 pour chaque itération de boucle.

11
rici

Je me permets d’être en désaccord avec l’opinion majoritaire, malgré la pleine compréhension que volatile signifie observable I/O.

Si vous avez ce code:

{
    volatile int x;
    x = 0;
}

Je crois que le compilateur peut l’optimise sous la règle règle as-if, en supposant que:

  1. La variable volatile n’est par ailleurs pas rendue visible de l’extérieur par ex. des pointeurs (ce qui n'est évidemment pas un problème ici car il n'y a rien de tel dans l'étendue donnée)

  2. Le compilateur ne vous fournit pas de mécanisme pour accéder en externe à cette volatile

La raison est simplement que vous ne pouviez pas observer la différence de toute façon, à cause du critère n ° 2.

Cependant, dans votre compilateur, le critère n ° 2 peut ne pas être satisfait! Le compilateur peut essayer de vous fournir des garanties supplémentaires concernant l'observation des variables volatile à partir de "l'extérieur", par exemple en analysant la pile. Dans de telles situations, le comportement est réellement observable et ne peut donc pas être optimisé.

Maintenant, la question est de savoir si le code suivant est différent de celui ci-dessus.

{
    volatile int x = 0;
}

Je pense avoir observé un comportement différent dans Visual C++ en ce qui concerne l'optimisation, mais je ne suis pas tout à fait sûr de savoir pourquoi. Il se peut que l’initialisation ne compte pas comme "accès"? Je ne suis pas sûr. Cela peut valoir une question distincte si vous êtes intéressé, mais sinon, je pense que la réponse est celle que j'ai expliquée ci-dessus.

9
Mehrdad

Théoriquement, un gestionnaire d'interruption pourrait

  • vérifiez si l'adresse de retour est comprise dans la fonction fn(). Il peut accéder à la table des symboles ou aux numéros de ligne source via une instrumentation ou des informations de débogage attachées.
  • changez ensuite la valeur de x, qui serait stockée à un décalage prévisible par rapport au pointeur de pile.

… Faisant ainsi fn() renvoyer une valeur différente de zéro.

6
berendi

Je vais simplement ajouter une référence détaillée pour la règle as-if et le mot clé volatile . (Au bas de ces pages, suivez les instructions "voir aussi" et "Références" pour revenir aux spécifications d'origine, mais je trouve cppreference.com beaucoup plus facile à lire/à comprendre.)

En particulier, je veux que vous lisiez cette section

objet volatile - un objet dont le type est qualifié de volatil, ou un sous-objet d'un objet volatil, ou un sous-objet modifiable d'un objet const-volatile. Chaque accès (opération de lecture ou d’écriture, appel de fonction membre, etc.) effectué via une expression glvalue de type qualifié de volatile est traité comme un effet secondaire visible aux fins d’optimisation (c’est-à-dire que, dans un seul thread d’exécution, volatile les accès ne peuvent pas être optimisés en sortie ou réorganisés avec un autre effet secondaire visible séquencé avant ou après l'accès volatil, ce qui rend les objets volatiles adaptés à la communication avec un gestionnaire de signaux, mais pas avec un autre thread d'exécution, voir std :: memory_order ) Toute tentative de référence à un objet volatil via une valeur gl nonue (par exemple via une référence ou un pointeur sur un type non volatile) entraîne un comportement indéfini.

Donc, le mot clé volatile en particulier concerne la désactivation de l'optimisation du compilateur sur glvalues . La seule chose que le mot clé volatile peut affecter est peut-être éventuellement return x, le compilateur peut faire ce qu'il veut avec le reste de la fonction.

La capacité du compilateur à optimiser le retour dépend de la capacité de celui-ci à optimiser l'accès de x dans ce cas (puisqu'il ne réorganise rien et qu'il ne supprime pas à proprement parler l'expression de retour. Il y a l'accès , mais il lit et écrit dans la pile, ce qui devrait pouvoir être rationalisé.) Donc, tel que je le lis, c’est une zone grise dans la mesure où le compilateur est autorisé à optimiser, et qui peut facilement être argumentée dans les deux sens.

Note latérale: dans ces cas, supposez toujours que le compilateur fera le contraire de ce que vous vouliez/avez besoin. Vous devez soit désactiver l'optimisation (au moins pour ce module), soit essayer de trouver un comportement plus défini pour ce que vous voulez. (C’est aussi pourquoi les tests unitaires sont si importants) Si vous pensez que c’est un défaut, vous devriez le signaler aux développeurs de C++.


Tout cela reste très difficile à lire, alors essayez d’inclure ce qui me semble pertinent afin que vous puissiez le lire vous-même.

glvalue Une expression de glvalue est lvalue ou xvalue.

Propriétés:

Une glvalue peut être implicitement convertie en une valeur avec une conversion implicite de lvalue à rvalue, de tableau à pointeur ou de fonction à pointeur. Une glvalue peut être polymorphe: le type dynamique de l'objet identifié est différent du type statique de l'expression. Une glvalue peut avoir un type incomplet, lorsque cela est permis par l'expression.


xvalue Les expressions suivantes sont des expressions xvalue:

un appel de fonction ou une expression d'opérateur surchargée, dont le type de retour est rvalue reference to object, tel que std :: move (x); a [n], l'expression en indice intégrée, dans laquelle un opérande est un tableau rvalue; a.m, le membre de l'expression d'objet, où a est une valeur et r est un membre de données non statique de type non-référence; a. * mp, le pointeur sur le membre de l'expression d'objet, où a est une valeur rvalue et mp est un pointeur sur un membre de données; une ? b: c, l'expression conditionnelle ternaire pour certains b et c (voir la définition pour plus de détails); une expression cast pour rvalue une référence au type d'objet, tel que static_cast (x); toute expression désignant un objet temporaire, après matérialisation temporaire. (depuis C++ 17) Propriétés:

Identique à rvalue (ci-dessous). Identique à glvalue (ci-dessous). En particulier, comme toutes les valeurs, les valeurs x se lient à des références de valeur et, comme toutes les valeurs glval, les valeurs x peuvent être polymorphes et les valeurs xclans peuvent être qualifiées de cv.


lvalue Les expressions suivantes sont des expressions lvalue:

le nom d'une variable, d'une fonction ou d'un membre de données, quel que soit son type, tel que std :: cin ou std :: endl. Même si le type de la variable est rvalue reference, l'expression constituée de son nom est une expression lvalue; un appel de fonction ou une expression d'opérateur surchargée, dont le type de retour est lvalue reference, tel que std :: getline (std :: cin, str), std :: cout << 1, str1 = str2 ou ++ it; a = b, a + = b, a% = b et toutes les autres expressions d'affectation et d'assignation composées intégrées; ++ a et --a, les expressions intégrées de pré-incrémentation et de pré-décrémentation; * p, l'expression indirection intégrée; a [n] et p [n], les expressions intégrées en indice, sauf où a est un tableau rvalue (depuis C++ 11); a.m, membre de l'expression d'objet, sauf où m est un énumérateur de membre ou une fonction membre non statique, ou où a est une valeur rvalue et m est un membre de données non statique de type non référence; p-> m, le membre intégré de l'expression du pointeur, sauf où m est un énumérateur de membre ou une fonction membre non statique; a. * mp, le pointeur sur le membre de l'expression d'objet, où a est une lvalue et mp est un pointeur sur un membre de données; p -> * mp, le pointeur intégré au membre d'une expression de pointeur, où mp est un pointeur au membre de données; a, b, l'expression de virgule intégrée, où b est une lvalue; une ? b: c, l'expression conditionnelle ternaire pour certains b et c (par exemple, lorsque les deux sont des valeurs du même type, mais voir la définition pour plus de détails); un littéral de chaîne, tel que "Hello, world!"; une expression de conversion vers le type de référence lvalue, tel que static_cast (x); un appel de fonction ou une expression d'opérateur surchargée, dont le type de retour est rvalue reference to function; une expression cast pour rvalue une référence au type de fonction, tel que static_cast (x). (depuis C++ 11) Propriétés:

Identique à glvalue (ci-dessous). L'adresse d'une lvalue peut être prise: & ++ i 1 et & std :: endl sont des expressions valides. Une valeur modifiable peut être utilisée en tant qu'opérande gauche des opérateurs d'affectation intégrée et d'affectation composée. Une lvalue peut être utilisée pour initialiser une référence de lvalue; cela associe un nouveau nom à l'objet identifié par l'expression.


règle as-if

Le compilateur C++ est autorisé à apporter des modifications au programme tant que les conditions suivantes restent vraies:

1) A chaque point de la séquence, les valeurs de tous les objets volatils sont stables (les évaluations précédentes sont terminées, les nouvelles évaluations ne sont pas démarrées) (jusqu'au C++ 11) 1) Les accès (en lecture et en écriture) aux objets volatils sont strictement conformes à la sémantique des expressions dans lesquelles ils se produisent. En particulier, ils ne sont pas réorganisés par rapport à d'autres accès volatils sur le même thread. (depuis C++ 11) 2) À la fin du programme, les données écrites dans les fichiers sont exactement comme si le programme était exécuté tel qu'il était écrit. 3) Le texte d’invitation envoyé aux appareils interactifs sera affiché avant que le programme n’attende sa saisie. 4) Si le pragma ISO C #pragma STDC FENV_ACCESS est pris en charge et est défini sur ON, les modifications apportées à l'environnement en virgule flottante (exceptions en virgule flottante et modes d'arrondi) sont garanties par les opérateurs et la fonction arithmétiques en virgule flottante. les appels comme s'ils étaient exécutés tels quels, sauf que le résultat de toute expression à virgule flottante autre que la conversion et l'affectation peut avoir une plage et une précision d'un type à virgule flottante différent du type de l'expression (voir FLT_EVAL_METHOD) nonobstant ce qui précède, résultats intermédiaires de n'importe quelle expression en virgule flottante peut être calculée comme si l'étendue et la précision étaient infinies (sauf si #pragma STDC FP_CONTRACT est désactivé)


Si vous voulez lire les spécifications, je crois que ce sont celles que vous devez lire.

Références

Norme C11 (ISO/IEC 9899: 2011): 6.7.3 Qualificateurs de type (p: 121-123)

Norme C99 (ISO/IEC 9899: 1999): 6.7.3 Qualificateurs de type (p: 108-110)

Norme C89/C90 (ISO/IEC 9899: 1990): 3.5.3 Qualificateurs de type

6
Tezra