158 votes

Optimiser l'élimination d'un "while(1) ;" en C++0x

Mis à jour, voir ci-dessous !

J'ai entendu et lu que C++0x permet au compilateur d'imprimer "Hello" pour le snippet suivant

#include <iostream>

int main() {
  while(1) 
    ;
  std::cout << "Hello" << std::endl;
}

Cela a apparemment quelque chose à voir avec les fils et les capacités d'optimisation. Il me semble que cela peut surprendre beaucoup de gens cependant.

Quelqu'un a-t-il une bonne explication de la raison pour laquelle il était nécessaire d'autoriser cela ? Pour référence, le projet C++0x le plus récent dit à 6.5/5

Une boucle qui, en dehors de l'instruction for-init dans le cas d'une instruction for,

  • ne fait aucun appel aux fonctions d'E/S de la bibliothèque, et
  • n'accède pas aux objets volatils et ne les modifie pas, et
  • n'effectue pas d'opérations de synchronisation (1.10) ou d'opérations atomiques (Clause 29)

peut être supposé par l'implémentation se terminer. [Note : Ceci a pour but de permettre des transformations du compilateur, telles que la suppression des boucles vides, même lorsque l'implémentation se termine. mations du compilateur, telles que la suppression des boucles vides, même lorsque la terminaison ne peut être prouvée. - note de fin ]

Editar:

Cet article perspicace dit à propos de ce texte sur les normes

Malheureusement, les mots "comportement indéfini" ne sont pas utilisés. Cependant, chaque fois que la norme dit "le compilateur peut supposer P", il est sous-entendu qu'un programme qui a la propriété not-P a une sémantique indéfinie.

Est-ce correct, et le compilateur est-il autorisé à imprimer "Bye" pour le programme ci-dessus ?


Il y a un encore plus perspicace fil ici qui traite d'un changement analogue au C, lancé par le type de l'article ci-dessus. Entre autres faits utiles, ils présentent une solution qui semble s'appliquer également à C++0x ( Mise à jour : Cela ne fonctionnera plus avec n3225 - voir plus bas !)

endless:
  goto endless;

Il semble qu'un compilateur ne soit pas autorisé à optimiser cela, car il ne s'agit pas d'une boucle, mais d'un saut. Un autre type résume le changement proposé dans C++0x et C201X

En écrivant une boucle, le programmeur affirme soit que la boucle fait quelque chose avec un comportement visible (effectuer des E/S, accéder à des objets volatils, ou effectue des opérations de synchronisation ou atomiques), o qu'il finit par se terminer. Si je viole cette hypothèse en écrivant une boucle infinie sans effet secondaire, je mens au compilateur et compilateur, et le comportement de mon programme est indéfini. (Si j'ai de la chance, le compilateur pourrait m'avertir à ce sujet). Le langage ne fournit pas (ne fournit plus ?) un moyen d'exprimer une boucle infinie sans comportement visible. comportement visible.


Mise à jour le 3.1.2011 avec n3225 : Le comité a déplacé le texte à 1.10/24 et dit

L'implémentation peut supposer que tout thread fera finalement l'une des choses suivantes :

  • résilier,
  • faire un appel à une fonction d'E/S de la bibliothèque,
  • accéder ou modifier un objet volatil, ou
  • effectuer une opération de synchronisation ou une opération atomique.

El goto l'astuce sera no plus travailler !

0 votes

L'auto-parallélisation peut-être ? J'aimerais en savoir plus mais éliminer cette boucle est certainement équivalent à l'exécuter sur un thread parallèle qui ne rapporte jamais. D'un autre côté, un thread qui génère des informations pour l'appelant serait imprégné d'une sorte de synchronisation. Et un thread avec des effets secondaires appropriés ne pourrait pas être parallélisé.

0 votes

Dans le cas général, cette boucle serait une erreur de programmation.

4 votes

while(1) { MyMysteriousFunction(); } doit pouvoir être compilé indépendamment sans connaître la définition de cette mystérieuse fonction, non ? Alors comment pouvons-nous déterminer si elle fait appel à des fonctions d'E/S de la bibliothèque ? En d'autres termes, le premier point pourrait sûrement être formulé ainsi ne fait aucun appel aux fonctions .

46voto

Philip Potter Points 6152

Pour moi, la justification pertinente est :

Ceci est destiné à permettre des transformations du compilateur, telles que la suppression des boucles vides, même lorsque la terminaison ne peut être prouvée.

Vraisemblablement, c'est parce que prouver la terminaison mécaniquement est difficile De plus, l'impossibilité de prouver la terminaison entrave les compilateurs qui pourraient autrement effectuer des transformations utiles, telles que le déplacement d'opérations non dépendantes d'avant la boucle à après ou vice versa, l'exécution d'opérations post-boucle dans un thread pendant que la boucle s'exécute dans un autre, etc. Sans ces transformations, une boucle pourrait bloquer tous les autres threads pendant qu'ils attendent que le seul thread termine ladite boucle. (J'utilise le terme "thread" au sens large pour désigner toute forme de traitement parallèle, y compris les flux d'instructions VLIW séparés).

EDIT : Exemple idiot :

while (complicated_condition()) {
    x = complicated_but_externally_invisible_operation(x);
}
complex_io_operation();
cout << "Results:" << endl;
cout << x << endl;

Ici, il serait plus rapide pour un seul thread de faire le complex_io_operation tandis que l'autre effectue tous les calculs complexes dans la boucle. Mais sans la clause que vous avez citée, le compilateur doit prouver deux choses avant de pouvoir effectuer l'optimisation : 1) que complex_io_operation() ne dépend pas des résultats de la boucle, et 2) que la boucle se termine . Prouver 1) est assez facile, prouver 2) est le problème de halting. Avec la clause, on peut supposer que la boucle se termine et obtenir une victoire de la parallélisation.

J'imagine également que les concepteurs ont considéré que les cas où les boucles infinies se produisent dans le code de production sont très rares et qu'il s'agit généralement de choses comme des boucles pilotées par des événements qui accèdent aux E/S d'une manière ou d'une autre. En conséquence, ils ont pessimisé le cas rare (boucles infinies) en faveur de l'optimisation du cas le plus courant (boucles non infinies, mais difficiles à prouver mécaniquement non infinies).

Cela signifie toutefois que les boucles infinies utilisées dans les exemples d'apprentissage en pâtiront et poseront des problèmes dans le code des débutants. Je ne peux pas dire que ce soit entièrement une bonne chose.

EDIT : en ce qui concerne l'article perspicace que vous mettez en lien, je dirais que "le compilateur peut supposer X à propos du programme" est logiquement équivalent à "si le programme ne satisfait pas X, le comportement est indéfini". Nous pouvons le montrer comme suit : supposons qu'il existe un programme qui ne satisfait pas la propriété X. Où le comportement de ce programme serait-il défini ? La norme ne définit le comportement que si la propriété X est vraie. Bien que la norme ne déclare pas explicitement le comportement non défini, elle l'a déclaré non défini par omission.

Considérons un argument similaire : "le compilateur peut supposer qu'une variable x n'est assignée qu'au maximum une fois entre deux points de séquence" est équivalent à "assigner à x plus d'une fois entre deux points de séquence est indéfini".

0 votes

"Prouver 1) est assez facile" - en fait, est-ce que cela ne découle pas immédiatement des 3 conditions pour que le compilateur soit autorisé à supposer la fin de la boucle sous la clause que Johannes demande ? Je pense qu'elles se résument à "la boucle n'a pas d'effet observable, sauf peut-être de tourner éternellement", et la clause assure que "tourner éternellement" n'est pas un comportement garanti pour de telles boucles.

0 votes

@Steve : c'est facile si la boucle ne se termine pas ; mais si la boucle se termine, elle peut avoir un comportement non trivial qui affecte le traitement de l'information de l'utilisateur. complex_io_operation .

0 votes

Oups, oui, j'ai oublié qu'il pourrait modifier les locaux non-volatils/alias/quelque chose qui sont utilisés dans l'opération IO. Vous avez donc raison : bien que cela ne s'ensuive pas nécessairement, il existe de nombreux cas dans lesquels les compilateurs peuvent prouver et prouvent qu'une telle modification ne se produit pas.

16voto

jalf Points 142628

Je pense que l'interprétation correcte est celle de votre édition : les boucles infinies vides sont un comportement indéfini.

Je ne dirais pas que c'est un comportement particulièrement intuitif, mais cette interprétation a plus de sens que l'autre, selon laquelle le compilateur est arbitrairement autorisé à ignorer boucles infinies sans invoquer UB.

Si les boucles infinies sont UB, cela signifie simplement que les programmes non terminés ne sont pas considérés comme significatifs : selon C++0x, ils tienen pas de sémantique.

Cela a aussi un certain sens. Il s'agit d'un cas particulier, où un certain nombre d'effets secondaires ne se produisent plus (par exemple, rien n'est jamais retourné de la commande main ), et un certain nombre d'optimisations du compilateur sont entravées par la nécessité de préserver les boucles infinies. Par exemple, déplacer les calculs à travers la boucle est parfaitement valable si la boucle n'a pas d'effets secondaires, parce que finalement, le calcul sera effectué dans tous les cas. Mais si la boucle ne se termine jamais, nous ne pouvons pas réarranger le code à travers elle en toute sécurité, parce que nous avons besoin d'un code de base. pourrait il suffit de changer les opérations qui sont effectivement exécutées avant que le programme ne se bloque. À moins que nous ne considérions un programme suspendu comme un UB.

8 votes

"Les boucles infinies vides sont un comportement indéfini" ? Alan Turing ne serait pas d'accord, mais seulement quand il aura fini de se retourner dans sa tombe.

11 votes

@Donal : Je n'ai jamais rien dit sur sa sémantique dans une machine de Turing. Nous discutons de la sémantique d'une boucle infinie sans effets secondaires. en C++ . Et tel que je le lis, C++0x choisit de dire que de telles boucles sont indéfinies.

0 votes

Les boucles infinies vides seraient stupides, et il n'y aurait aucune raison d'avoir des règles spéciales pour elles. La règle est conçue pour traiter les boucles utiles d'une durée non limitée (espérons-le non infinie), qui calculent quelque chose qui sera nécessaire dans le futur mais pas immédiatement.

10voto

Daniel Newby Points 1485

La question pertinente est que le compilateur est autorisé à réordonner le code dont les effets secondaires ne sont pas en conflit. L'ordre d'exécution surprenant pourrait se produire même si le compilateur produisait un code machine non terminal pour la boucle infinie.

Je pense que c'est la bonne approche. La spécification du langage définit les moyens de faire respecter l'ordre d'exécution. Si vous voulez une boucle infinie qui ne peut pas être réordonnée, écrivez ceci :

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");

2 votes

@JohannesSchaub-litb : Si une boucle - sans fin ou non - ne lit ou n'écrit aucune variable volatile pendant l'exécution, et n'appelle aucune fonction qui pourrait le faire, un compilateur est libre de différer toute portion de la boucle jusqu'au premier effort pour accéder à quelque chose qui y est calculé. Étant donné unsigned int dummy; while(1){dummy++;} fprintf(stderror,"Hey\r\n"); fprintf(stderror,"Result was %u\r\n",dummy); le premier fprintf pourrait s'exécuter, mais pas le second (le compilateur pourrait déplacer le calcul de dummy entre les deux fprintf mais pas au-delà de celui qui imprime sa valeur).

0 votes

BTW, depuis que j'ai répondu à cette question, j'ai découvert que dans certains cas où rien dans le code comme écrit s'appuiera sur toute post-condition qui serait garantie par l'achèvement réussi d'une boucle, clang combinera parfois les optimisations qui s'appuient sur de telles post-conditions avec des optimisations qui suppriment la boucle. La norme n'interdit pas de telles absurdités, mais cela signifie que les entrées qui feraient en sorte que le code soit bloqué dans une boucle infinie peuvent provoquer une corruption de la mémoire, d'une manière qui n'est pas liée aux lois ordinaires de la causalité.

8voto

linuxuser27 Points 4095

Je pense que c'est dans la lignée de ce type de question qui fait référence à un autre filetage . L'optimisation peut occasionnellement supprimer les boucles vides.

3 votes

Bonne question. Il semble que ce type avait exactement le problème que ce paragraphe permet à ce compilateur de causer. Dans la discussion liée par l'une des réponses, il est écrit que " Malheureusement, les mots " comportement indéfini " ne sont pas utilisés. Cependant, chaque fois que la norme dit 'le compilateur peut supposer P', il est sous-entendu qu'un programme qui a la propriété not-P a une sémantique indéfinie." . Cela me surprend. Cela signifie-t-il que mon programme d'exemple ci-dessus a un comportement non défini, et qu'il peut se planter de nulle part ?

0 votes

@Johannes : le texte "may be assumed" n'apparaît nulle part ailleurs dans le projet que j'ai sous la main, et "may assume" n'apparaît que deux fois. Bien que j'aie vérifié cela avec une fonction de recherche qui ne parvient pas à faire correspondre les retours à la ligne, il se peut que j'en aie manqué. Je ne suis donc pas sûr que la généralisation de l'auteur soit justifiée au vu des preuves. mais en tant que mathématicien, je dois concéder la logique de l'argument, à savoir que si le compilateur suppose quelque chose de faux, alors en général il peut déduire n'importe quoi...

0 votes

...Permettre une contradiction dans le raisonnement du compilateur sur le programme fait certainement très fortement allusion à UB, puisqu'en particulier cela permet au compilateur, pour tout X, de déduire que le programme est équivalent à X. Il est certain que permettre au compilateur de déduire qui lui permet de hacer que. Je suis d'accord avec l'auteur, aussi, que si UB est prévu, il devrait être explicitement indiqué, et si ce n'est pas prévu, alors le texte de la spécification est erroné et devrait être corrigé (peut-être par l'équivalent en langage de spécification de, "le compilateur peut remplacer la boucle avec du code qui n'a aucun effet", je ne suis pas sûr).

-1voto

spraff Points 10492

Je pense qu'il est utile de souligner que des boucles qui seraient infinies, à l'exception du fait qu'elles interagissent avec d'autres threads via des variables non volatiles et non synchronisées, peuvent maintenant donner un comportement incorrect avec un nouveau compilateur.

En d'autres termes, rendez vos globaux volatils, ainsi que les arguments passés dans une telle boucle par pointeur/référence.

0 votes

S'ils interagissent avec d'autres threads, vous ne devriez pas les rendre volatiles, les rendre atomiques ou les protéger avec un verrou.

1 votes

C'est un mauvais conseil. Les faire volatile n'est ni nécessaire ni suffisante, et elle nuit considérablement aux performances.

0 votes

@DavidSchwartz : La sémantique de volatile sont "définis par l'implémentation", et la suffisance de ce qualificatif à des fins diverses dépendra de la manière dont une implémentation choisit de le définir. Il y a beaucoup d'implémentations indépendantes pour lesquelles volatile est à la fois nécessaire et suffisant pour accomplir ce que les programmes devront faire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X