Le niveau d'optimisation -O3 est-il dangereux dans g ++?

233

J'ai entendu de diverses sources (bien que principalement d'un de mes collègues), que la compilation avec un niveau d'optimisation -O3en g ++ est en quelque sorte «dangereuse», et devrait être évitée en général à moins que cela ne soit prouvé.

Est-ce vrai, et si oui, pourquoi? Dois-je rester fidèle -O2?

Dunnie
la source
38
Ce n'est dangereux que si vous comptez sur un comportement non défini. Et même alors, je serais surpris que ce soit le niveau d'optimisation qui gâche quelque chose.
Seth Carnegie
5
Le compilateur est toujours contraint de produire un programme qui se comporte "comme si" il compilait exactement votre code. Je ne sais pas qui -O3est considéré comme particulièrement buggy? Je pense que cela peut peut-être aggraver un comportement indéfini, car il peut faire des choses étranges et merveilleuses en fonction de certaines hypothèses, mais ce serait de votre faute. Donc en général, je dirais que ça va.
BoBTFish
5
Il est vrai que des niveaux d'optimisation plus élevés sont plus sujets aux bogues du compilateur. J'ai moi-même touché quelques cas, mais en général, ils sont encore assez rares.
Mysticial
21
-O2s'allume -fstrict-aliasing, et si votre code survit, il survivra probablement à d'autres optimisations, car c'est une erreur que les gens se trompent encore et encore. Cela dit, ce -fpredictive-commoningn'est que dans -O3, et l'activation pourrait activer des bogues dans votre code causés par des hypothèses incorrectes sur la concurrence. Moins votre code est erroné, moins l'optimisation est dangereuse ;-)
Steve Jessop
6
@PlasmaHH, je ne pense pas que "plus strict" soit une bonne description de -Ofastcela, cela désactive par exemple la gestion des NaNs conforme à l'IEEE
Jonathan Wakely

Réponses:

223

Au début de gcc (2.8 etc.) et à l'époque des egcs, et redhat 2.96 -O3 était parfois assez buggé. Mais cela fait plus d'une décennie, et -O3 n'est pas très différent des autres niveaux d'optimisations (en buggyness).

Il a cependant tendance à révéler des cas où les gens s'appuient sur un comportement indéfini, car ils s'appuient plus strictement sur les règles, et en particulier les cas de coin, de la ou des langues.

À titre personnel, j'exécute depuis plusieurs années un logiciel de production dans le secteur financier avec -O3 et je n'ai pas encore rencontré de bug qui n'aurait pas existé si j'aurais utilisé -O2.

À la demande générale, voici un ajout:

-O3 et en particulier des indicateurs supplémentaires tels que -funroll-loops (non activé par -O3) peuvent parfois entraîner la génération de plus de code machine. Dans certaines circonstances (par exemple sur un processeur avec un cache d'instructions L1 exceptionnellement petit), cela peut provoquer un ralentissement en raison de tout le code, par exemple d'une boucle interne qui ne correspond plus à L1I. Généralement, gcc essaie assez fort de ne pas générer autant de code, mais comme il optimise généralement le cas générique, cela peut arriver. Les options particulièrement sujettes à cela (comme le déroulement de boucle) ne sont normalement pas incluses dans -O3 et sont marquées en conséquence dans la page de manuel. En tant que tel, c'est généralement une bonne idée d'utiliser -O3 pour générer du code rapide, et de ne retomber sur -O2 ou -Os (qui essaie d'optimiser la taille du code) que lorsque cela est approprié (par exemple lorsqu'un profileur indique que L1I manque).

Si vous souhaitez pousser l'optimisation à l'extrême, vous pouvez modifier dans gcc via --param les coûts associés à certaines optimisations. Notez également que gcc a maintenant la possibilité de mettre des attributs sur des fonctions qui contrôlent les paramètres d'optimisation uniquement pour ces fonctions, donc lorsque vous trouvez que vous avez un problème avec -O3 dans une fonction (ou que vous voulez essayer des drapeaux spéciaux pour cette fonction uniquement), vous n'avez pas besoin de compiler tout le fichier ou même tout le projet avec O2.

otoh il semble que des précautions doivent être prises lors de l'utilisation de -Ofast, qui stipule:

-Ofast permet toutes les optimisations -O3. Il permet également des optimisations qui ne sont pas valides pour tous les programmes conformes aux normes.

ce qui me fait conclure que -O3 est censé être entièrement conforme aux normes.

PlasmaHH
la source
2
J'utilise juste quelque chose comme le contraire. J'utilise toujours -Os ou -O2 (parfois O2 génère un exécutable plus petit).
CoffeDeveloper
3
Je fais ça pour la vitesse. La plupart du temps, O3 ralentit les choses. Je ne sais pas exactement pourquoi, je soupçonne que cela pollue le cache d'instructions.
CoffeDeveloper du
4
@DarioOO J'ai l'impression que plaider "le ballonnement de code" est une chose populaire à faire, mais je ne le vois presque jamais soutenu par des repères. Cela dépend beaucoup de l'architecture, mais chaque fois que je vois des benchmarks publiés (par exemple phoronix.com/… ), cela montre que l'O3 est plus rapide dans la grande majorité des cas. J'ai vu le profilage et l'analyse minutieuse nécessaires pour prouver que le gonflement du code était en fait un problème, et cela ne se produit généralement que pour les personnes qui adoptent les modèles de manière extrême.
Nir Friedman
1
@NirFriedman: Cela a tendance à avoir un problème lorsque le modèle de coût en ligne du compilateur a des bogues, ou lorsque vous optimisez pour une cible totalement différente de celle sur laquelle vous exécutez. Chose intéressante, cela s'applique à tous les niveaux d'optimisation ...
PlasmaHH
1
@PlasmaHH: le problème d'utilisation de cmov serait difficile à résoudre pour le cas général. Habituellement, vous n'avez pas simplement trié vos données, donc lorsque gcc essaie de décider si une branche est prévisible ou non, l'analyse statique à la recherche d'appels à des std::sortfonctions est peu susceptible de vous aider. Utiliser quelque chose comme stackoverflow.com/questions/109710/… pourrait aider, ou peut-être écrire la source pour tirer parti du tri: scan jusqu'à ce que vous voyez> = 128, puis commencez à additionner. En ce qui concerne le code gonflé, oui, j'ai l'intention de le signaler. : P
Peter Cordes
42

Dans mon expérience quelque peu mouvementée, appliquer -O3à un programme entier le rend presque toujours plus lent (par rapport à -O2), car il active le déroulement et l'incrustation de boucle agressifs qui font que le programme ne tient plus dans le cache d'instructions. Pour les programmes plus importants, cela peut également être vrai par -O2rapport à -Os!

Le modèle d'utilisation prévu pour -O3, après le profilage de votre programme, vous l'appliquez manuellement à une petite poignée de fichiers contenant des boucles internes critiques qui bénéficient réellement de ces compromis agressifs espace-pour-vitesse. Les nouvelles versions de GCC ont un mode d'optimisation guidé par profil qui peut (IIUC) appliquer sélectivement les -O3optimisations aux fonctions chaudes - automatisant efficacement ce processus.

zwol
la source
10
"presque toujours"? Faites-en "50-50", et nous aurons un accord ;-).
No-Bugs Hare le
12

L'option -O3 active des optimisations plus coûteuses, telles que la fonction inline, en plus de toutes les optimisations des niveaux inférieurs «-O2» et «-O1». Le niveau d'optimisation «-O3» peut augmenter la vitesse de l'exécutable résultant, mais peut également augmenter sa taille. Dans certaines circonstances où ces optimisations ne sont pas favorables, cette option peut en fait ralentir un programme.

neel
la source
3
Je comprends que certaines "optimisations apparentes" pourraient ralentir un programme, mais avez-vous une source qui prétend que GCC -O3 a ralenti un programme?
Mooing Duck
1
@MooingDuck: Bien que je ne puisse pas citer une source, je me souviens avoir rencontré un tel cas avec des processeurs AMD plus anciens qui avaient un assez petit cache L1I (instructions ~ 10k). Je suis sûr que Google en a plus pour les personnes intéressées, mais en particulier les options telles que le déroulement des boucles ne font pas partie d'O3, et ces tailles augmentent beaucoup. -Os est celui pour lequel vous voulez rendre le plus petit exécutable. Même -O2 peut augmenter la taille du code. Un bon outil pour jouer avec le résultat de différents niveaux d'optimisation est l'explorateur gcc.
PlasmaHH
@PlasmaHH: En fait, une petite taille de cache est quelque chose qu'un compilateur pourrait bousiller, bon point. Voilà un très bon exemple. Veuillez le mettre dans la réponse.
Mooing Duck
1
@PlasmaHH Pentium III avait un cache de code de 16 Ko. Le K6 et supérieur d'AMD avait en fait un cache d'instructions de 32 Ko. Les P4 ont commencé avec une valeur d'environ 96 Ko. Le Core I7 dispose en fait d'un cache de code L1 de 32 Ko. Les décodeurs d'instructions sont forts de nos jours, donc votre L3 est assez bon pour se replier sur presque toutes les boucles.
doug65536
1
Vous verrez une énorme augmentation des performances chaque fois qu'une fonction est appelée dans une boucle et elle peut éliminer considérablement la sous-expression commune et lever le recalcul inutile de la fonction avant la boucle.
doug65536
8

Oui, O3 est plus bogué. Je suis un développeur de compilateur et j'ai identifié des bogues gcc clairs et évidents causés par O3 générant des instructions d'assemblage SIMD buggy lors de la construction de mon propre logiciel. D'après ce que j'ai vu, la plupart des logiciels de production sont livrés avec O2, ce qui signifie que O3 recevra moins d'attention par rapport aux tests et aux corrections de bogues.

Pensez-y de cette façon: O3 ajoute plus de transformations au-dessus de O2, ce qui ajoute plus de transformations au-dessus de O1. Statistiquement parlant, plus de transformations signifient plus de bugs. C'est vrai pour tout compilateur.

David Yeager
la source
3

Récemment, j'ai rencontré un problème d'utilisation de l'optimisation avec g++. Le problème était lié à une carte PCI, où les registres (pour les commandes et les données) étaient représentés par une adresse mémoire. Mon pilote a mappé l'adresse physique à un pointeur dans l'application et l'a donnée au processus appelé, qui a fonctionné comme ceci:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

La carte n'a pas fonctionné comme prévu. Quand j'ai vu l'ensemble , je compris que le compilateur écrit uniquement someCommand[ the last ]en pciMemoryomettant toutes les écritures précédentes.

En conclusion: soyez précis et attentif à l'optimisation.

borisbn
la source
38
Mais le point ici est que votre programme a simplement un comportement indéfini; l'optimiseur n'a rien fait de mal. En particulier, vous devez déclarer en pciMemorytant que volatile.
Konrad Rudolph
11
Ce n'est en fait pas UB mais le compilateur a le droit de supprimer toutes les écritures sauf la dernière, pciMemorycar toutes les autres écritures n'ont aucun effet. Pour l'optimiseur, c'est génial car il peut supprimer de nombreuses instructions inutiles et chronophages.
Konrad Rudolph
4
J'ai trouvé cela en standard (après 10+ ans))) - Une déclaration volatile peut être utilisée pour décrire un objet correspondant à un port d'entrée / sortie mappé en mémoire ou un objet accessible par une fonction d'interruption asynchrone. Les actions sur les objets ainsi déclarés ne doivent pas être «optimisées» par une implémentation ou réorganisées, sauf dans la mesure permise par les règles d'évaluation des expressions.
borisbn
2
@borisbn Un peu hors sujet mais comment savoir que votre appareil a pris la commande avant d'envoyer une nouvelle commande?
user877329
3
@ user877329 Je l'ai vu par le comportement de l'appareil, mais c'était une excellente quête
borisbn