web-dev-qa-db-fra.com

Optimisation loin "pendant (1);" en C ++ 0x

Mis à jour, voir ci-dessous !

J'ai entendu et lu que C++ 0x permet à un compilateur d'imprimer "Bonjour" pour l'extrait de code suivant

#include <iostream>

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

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

Quelqu'un at-il une bonne explication de la raison pour laquelle cela était nécessaire pour permettre? Pour référence, le brouillon C++ 0x le plus récent indique à 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 ni ne modifie les objets volatils, et
  • n'effectue aucune opération de synchronisation (1.10) ou atomique (article 29)

peut être supposé par l'implémentation se terminer. [Remarque: Ceci est destiné à permettre les transformations du compilateur, telles que la suppression de boucles vides, même lorsque la terminaison ne peut pas être prouvée. - note de fin]

Modifier:

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 implicite qu'un programme qui a la propriété not-P a une sémantique non définie.

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


Il y a encore plus de perspicacité fil ici , qui concerne un changement analogue à C, commencé par Guy a fait l'article lié ci-dessus. Entre autres faits utiles, ils présentent une solution qui semble également s'appliquer à C++ 0x ( Update: Cela ne fonctionnera plus avec n3225 - voir ci-dessous!)

endless:
  goto endless;

Un compilateur n'est pas autorisé à optimiser cela, semble-t-il, car ce n'est pas une boucle, mais un saut. Un autre gars 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 (effectue des E/S, accède à des objets volatils ou effectue des opérations de synchronisation ou atomiques), ou qu'il finit par se terminer. Si je viole cette hypothèse en écrivant une boucle infinie sans effets secondaires, je mens au compilateur et le comportement de mon programme n'est pas défini. (Si j'ai de la chance, le compilateur pourrait m'en avertir.) Le langage ne fournit pas (ne fournit plus?) Un moyen d'exprimer une boucle infinie sans comportement visible.


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

L'implémentation peut supposer que n'importe quel thread finira par effectuer l'une des opérations suivantes:

  • mettre fin,
  • appeler une fonction d'E/S de bibliothèque,
  • accéder ou modifier un objet volatil, ou
  • effectuer une opération de synchronisation ou une opération atomique.

L'astuce goto ne fonctionnera plus !

149

Quelqu'un at-il une bonne explication de la raison pour laquelle cela était nécessaire pour permettre?

Oui, Hans Boehm fournit une justification à cela dans N1528: Pourquoi un comportement indéfini pour les boucles infinies? , bien qu'il s'agisse du document WG14, la justification s'applique également au C++ et le document fait référence à la fois au WG14 et au WG21:

Comme N1509 le souligne correctement, le projet actuel donne essentiellement un comportement indéfini aux boucles infinies en 6.8.5p6. Un problème majeur pour cela est qu'il permet au code de se déplacer à travers une boucle potentiellement non terminante. Par exemple, supposons que nous ayons les boucles suivantes, où count et count2 sont des variables globales (ou ont eu leur adresse prise), et p est une variable locale, dont l'adresse n'a pas été prise:

for (p = q; p != 0; p = p -> next) {
    ++count;
}
for (p = q; p != 0; p = p -> next) {
    ++count2;
}

Ces deux boucles pourraient-elles être fusionnées et remplacées par la boucle suivante?

for (p = q; p != 0; p = p -> next) {
        ++count;
        ++count2;
}

Sans la dérogation spéciale en 6.8.5p6 pour les boucles infinies, cela serait interdit: si la première boucle ne se termine pas parce que q pointe vers une liste circulaire, l'original n'écrit jamais sur count2. Ainsi, il pourrait être exécuté en parallèle avec un autre thread qui accède ou met à jour count2. Ce n'est plus sûr avec la version transformée qui accède à count2 malgré la boucle infinie. Ainsi, la transformation introduit potentiellement une course aux données.

Dans des cas comme celui-ci, il est très peu probable qu'un compilateur soit en mesure de prouver la terminaison de boucle; il faudrait comprendre que q pointe vers une liste acyclique, qui, je crois, est au-delà des capacités de la plupart des compilateurs traditionnels, et souvent impossible sans des informations complètes sur le programme.

Les restrictions imposées par les boucles non terminales sont une restriction sur l'optimisation des boucles terminales pour lesquelles le compilateur ne peut pas prouver la terminaison, ainsi que sur l'optimisation des boucles réellement non terminales. Les premiers sont beaucoup plus courants que les seconds, et souvent plus intéressants à optimiser.

Il y a clairement aussi des boucles for avec une variable de boucle entière dans laquelle il serait difficile pour un compilateur de prouver la terminaison, et il serait donc difficile pour le compilateur de restructurer des boucles sans 6.8.5p6. Même quelque chose comme

for (i = 1; i != 15; i += 2)

ou

for (i = 1; i <= 10; i += j)

semble simple à gérer. (Dans le premier cas, une théorie des nombres de base est nécessaire pour prouver la terminaison, dans le dernier cas, nous devons savoir quelque chose sur les valeurs possibles de j pour ce faire. Le bouclage pour les entiers non signés peut compliquer davantage certains de ce raisonnement. )

Ce problème semble s'appliquer à presque toutes les transformations de restructuration de boucle, y compris les transformations de parallélisation du compilateur et d'optimisation du cache, qui sont toutes deux susceptibles de gagner en importance, et sont déjà souvent importantes pour le code numérique. Cela semble susceptible de se transformer en un coût substantiel au profit de la possibilité d'écrire des boucles infinies de la manière la plus naturelle possible, d'autant plus que la plupart d'entre nous écrivent rarement des boucles intentionnellement infinies.

La seule différence majeure avec C est que C11 fournit une exception pour contrôler les expressions qui sont des expressions constantes qui diffère de C++ et rend votre exemple spécifique bien défini en C11.

29
Shafik Yaghmour

Pour moi, la justification pertinente est:

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

Vraisemblablement, c'est parce que prouver mécaniquement la terminaison est difficile, et l'impossibilité de prouver la terminaison gêne les compilateurs qui pourraient autrement effectuer des transformations utiles, telles que déplacer des opérations non dépendantes d'avant la boucle vers après ou vice versa, effectuer une publication -opérations de boucle dans un thread tandis que la boucle s'exécute dans un autre, et ainsi de suite. Sans ces transformations, une boucle peut bloquer tous les autres threads en attendant que l'un d'eux termine cette boucle. (J'utilise vaguement "thread" pour désigner toute forme de traitement parallèle, y compris les flux d'instructions VLIW séparés.)

EDIT: exemple stupide:

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 thread de faire le complex_io_operation Tandis que l'autre fait tous les calculs complexes de 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 terminera. Prouver 1) est assez facile, prouver 2) est le problème d'arrêt. Avec la clause, il peut supposer que la boucle se termine et obtenir un gain de parallélisation.

J'imagine également que les concepteurs ont considéré que les cas où des boucles infinies se produisent dans le code de production sont très rares et sont généralement des choses comme des boucles événementielles 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 cependant que les boucles infinies utilisées dans les exemples d'apprentissage en souffriront et augmenteront les pièges dans le code débutant. Je ne peux pas dire que c'est entièrement une bonne chose.

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

Prenons un argument similaire: "le compilateur peut supposer qu'une variable x n'est affectée qu'à au plus une fois entre des points de séquence" équivaut à "affecter à x plus d'une fois entre des points de séquence n'est pas défini".

47
Philip Potter

Je pense que l'interprétation correcte est celle de votre montage: 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 est plus logique que l'alternative, que le compilateur est autorisé arbitrairement à ignorer boucles infinies sans invoquer UB.

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

Cela a aussi un certain sens. C'est un cas spécial, où un certain nombre d'effets secondaires ne se produisent plus (par exemple, rien n'est jamais renvoyé par main), et un certain nombre d'optimisations du compilateur sont gênées par la nécessité de conserver des boucles infinies. Par exemple, déplacer des calculs à travers la boucle est parfaitement valide si la boucle n'a pas d'effets secondaires, car finalement, le calcul sera effectué dans tous les cas. Mais si la boucle ne se termine jamais, nous ne pouvons pas réorganiser le code en toute sécurité, car nous pourrions juste changer les opérations réellement exécutées avant le blocage du programme. Sauf si nous traitons un programme suspendu comme UB, c'est-à-dire.

14
jalf

Le problème est que le compilateur est autorisé à réorganiser le code dont les effets secondaires n'entrent pas en conflit. L'ordre d'exécution surprenant pourrait se produire même si le compilateur produisait du code machine sans terminaison pour la boucle infinie.

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

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");
8
Daniel Newby

Je pense que c'est dans le sens de ce type de question , qui fait référence à un autre thread . L'optimisation peut parfois supprimer des boucles vides.

8
linuxuser27

Je pense que le problème pourrait peut-être être mieux énoncé, comme "Si un morceau de code ultérieur ne dépend pas d'un morceau de code antérieur, et que le morceau de code précédent n'a aucun effet secondaire sur aucune autre partie du système, la sortie du compilateur peut exécuter le dernier morceau de code avant, après ou mélangé avec l'exécution du premier, même si le premier contient des boucles, sans tenir compte du moment ou de la fin de l'ancien code. Par exemple, le compilateur pourrait réécrire:

 void testfermat (int n) 
 {
 int a = 1, b = 1, c = 1; 
 while (pow (a, n) + pow (b, n)! = pow (c, n)) 
 {
 if (b> a) a ++; sinon si (c> b) {a = 1; b ++}; else {a = 1; b = 1; c ++}; 
} 
 printf ("Le résultat est"); 
 printf ("% d /% d /% d", a, b, c); 
} 

comme

 void testfermat (int n) 
 {
 if (fork_is_first_thread ()) 
 {
 int a = 1, b = 1, c = 1; 
 Tandis que (pow (a, n) + pow (b, n)! = Pow (c, n)) 
 {
 If (b> a) a ++; sinon si (c> b) {a = 1; b ++}; else {a = 1; b = 1; c ++}; 
} 
 signal_other_thread_and_die (); 
} 
 else // Deuxième fil 
 {
 printf ("Le résultat est "); 
 wait_for_other_thread (); 
} 
 printf ("% d /% d /% d ", a, b, c); 
} 

Généralement pas déraisonnable, bien que je puisse craindre que:

 int total = 0; 
 pour (i = 0; num_reps> i; i ++) 
 {
 update_progress_bar (i); 
 total + = do_something_slow_with_no_side_effects (i); 
} 
 show_result (total); 

deviendrait

 int total = 0; 
 if (fork_is_first_thread ()) 
 {
 for (i = 0; num_reps> i; i ++) 
 total + = do_something_slow_with_no_side_effects (i); 
 signal_other_thread_and_die (); 
} 
 else 
 {
 for (i = 0; num_reps> i; i ++) 
 update_progress_bar (i); 
 wait_for_other_thread (); 
} 
 show_result (total); 

En ayant un processeur pour gérer les calculs et un autre pour gérer les mises à jour de la barre de progression, la réécriture améliorerait l'efficacité. Malheureusement, cela rendrait les mises à jour de la barre de progression plutôt moins utiles qu'elles ne devraient l'être.

1
supercat

Il n'est pas décidable pour le compilateur pour les cas non triviaux s'il s'agit d'une boucle infinie.

Dans différents cas, il peut arriver que votre optimiseur atteigne une meilleure classe de complexité pour votre code (par exemple, c'était O (n ^ 2) et vous obtenez O(n) ou O(1) après optimisation).

Ainsi, l'inclusion d'une telle règle interdisant la suppression d'une boucle infinie dans la norme C++ rendrait de nombreuses optimisations impossibles. Et la plupart des gens n'en veulent pas. Je pense que cela répond tout à fait à votre question.


Autre chose: je n'ai jamais vu d'exemple valable où vous avez besoin d'une boucle infinie qui ne fait rien.

Le seul exemple dont j'ai entendu parler était un vilain piratage qui devrait vraiment être résolu autrement: il s'agissait de systèmes embarqués où la seule façon de déclencher une réinitialisation était de geler l'appareil afin que le chien de garde le redémarre automatiquement.

Si vous connaissez un exemple valide/bon où vous avez besoin d'une boucle infinie qui ne fait rien, dites-le-moi.

0
Albert

Je pense qu'il vaut la peine de souligner que les 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 produire 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 via un pointeur/référence.

0
spraff