96 votes

Optimisation de G++ au-delà de -O3/-Ofast

Le problème

Nous avons un programme de taille moyenne pour une tâche de simulation, que nous devons optimiser. Nous avons déjà fait de notre mieux pour optimiser la source jusqu'à la limite de nos compétences en programmation, y compris le profilage avec la fonction Gprof y Valgrind .

Une fois terminé, nous voulons exécuter le programme sur plusieurs systèmes, probablement pendant quelques mois. Par conséquent, nous sommes vraiment intéressés à pousser l'optimisation jusqu'à ses limites.

Tous les systèmes fonctionneront sous Debian/Linux sur du matériel relativement récent (Intel i5 ou i7).

La question

Quelles sont les options d'optimisation possibles en utilisant une version récente de g++, qui vont au-delà de -O3/-Ofast ?

Nous sommes également intéressés par les petites optimisations coûteuses, qui seront payantes à long terme.

Ce que nous utilisons actuellement

Actuellement, nous utilisons les options d'optimisation g++ suivantes :

  • -Ofast : Niveau d'optimisation "standard" le plus élevé. Les éléments suivants sont inclus -ffast-math n'a pas posé de problème dans nos calculs, nous avons donc décidé de l'adopter, malgré sa non-conformité aux normes.
  • -march=native : Permettre l'utilisation de toutes les instructions spécifiques au CPU.
  • -flto pour permettre l'optimisation du temps de liaison, à travers différentes unités de compilation.

116voto

Pyves Points 1953

La plupart des réponses suggèrent des solutions alternatives, comme des compilateurs différents ou des bibliothèques externes, ce qui entraînerait très probablement beaucoup de travail de réécriture ou d'intégration. Je vais essayer de m'en tenir à ce que la question demande, et me concentrer sur ce qui peut être fait avec GCC seul, en activant des drapeaux de compilation ou en faisant des changements minimaux au code, comme demandé par le PO. Il ne s'agit pas d'une réponse "vous devez faire ceci", mais plutôt d'une collection d'astuces GCC qui ont bien fonctionné pour moi et que vous pouvez essayer si elles sont pertinentes dans votre contexte spécifique.


Avertissements concernant la question initiale

Avant d'entrer dans les détails, un petit avertissement concernant la question, typiquement pour les gens qui viendront, liront la question et diront "le PO optimise au-delà de O3, je devrais utiliser les mêmes drapeaux que lui !".

  • -march=native permet l'utilisation d instructions spécifiques à une architecture CPU donnée et qui ne sont pas nécessairement disponibles sur une architecture différente. Le programme peut ne pas fonctionner du tout s'il est exécuté sur un système doté d'une unité centrale différente, ou être significativement plus lent (car cela permet également à l'utilisateur d'avoir accès à d'autres fonctions). mtune=native ), il faut donc en tenir compte si vous décidez de l'utiliser. Plus d'informations aquí .
  • -Ofast comme vous l'avez dit, permet à certains non conforme aux normes Il faut donc l'utiliser avec prudence. Plus d'informations aquí .

Autres drapeaux GCC à essayer

Les détails pour les différents drapeaux sont listés aquí .

  • -Ofast permet à -ffast-math qui, à son tour, permet -fno-math-errno , -funsafe-math-optimizations , -ffinite-math-only , -fno-rounding-math , -fno-signaling-nans y -fcx-limited-range . Vous pouvez aller encore plus loin sur optimisations des calculs en virgule flottante en ajoutant sélectivement certains drapeaux supplémentaires comme -fno-signed-zeros , -fno-trapping-math et autres. Ceux-ci ne sont pas inclus dans -Ofast et peuvent donner quelques gains de performance supplémentaires sur les calculs, mais vous devez vérifier s'ils vous profitent réellement et ne cassent aucun calcul.
  • Le GCC comporte également une grande quantité de autres drapeaux d'optimisation qui ne sont pas activés par les options "-O". Elles sont listées comme des "options expérimentales qui peuvent produire du code cassé", donc encore une fois, elles doivent être utilisées avec prudence, et leurs effets vérifiés à la fois par des tests de correction et d'évaluation. Néanmoins, j'utilise souvent -frename-registers Cette option n'a jamais produit de résultats indésirables pour moi et tend à donner une augmentation sensible des performances (c'est-à-dire qu'elle peut être mesurée lors d'un benchmarking). C'est le type d'indicateur qui est très dépendant de votre processeur cependant. -funroll-loops donne aussi parfois de bons résultats (et implique aussi -frename-registers ), mais cela dépend de votre code réel.

PGO

Le CCG a Optimisations guidées par le profil caractéristiques. Il n'y a pas beaucoup de documentation précise de GCC à son sujet, mais néanmoins, le faire fonctionner est assez simple.

  • compilez d'abord votre programme avec -fprofile-generate .
  • laissez le programme s'exécuter (le temps d'exécution sera sensiblement plus lent car le code génère également des informations de profil dans des fichiers .gcda).
  • recompiler le programme avec -fprofile-use . Si votre application est multithreadée, ajoutez également l'option -fprofile-correction drapeau.

PGO avec GCC peut donner des résultats étonnants et améliorer les performances de manière significative (j'ai constaté une augmentation de 15 à 20 % de la vitesse sur l'un des projets sur lesquels je travaillais récemment). Évidemment, le problème ici est d'avoir des des données suffisamment représentatives de l'exécution de votre application, qui n'est pas toujours disponible ou facile à obtenir.

Le mode parallèle de GCC

GCC dispose d'un Mode parallèle qui a été publié à peu près au moment où le compilateur GCC 4.2 est sorti.

En gros, il vous fournit implémentations parallèles de nombreux algorithmes de la bibliothèque standard C++. . Pour les activer globalement, il suffit d'ajouter l'option -fopenmp et le -D_GLIBCXX_PARALLEL au compilateur. Vous pouvez également activer sélectivement chaque algorithme lorsque cela est nécessaire, mais cela nécessitera quelques modifications mineures du code.

Toutes les informations sur ce mode parallèle se trouvent dans le site suivant aquí .

Si vous utilisez fréquemment ces algorithmes sur de grandes structures de données et que vous disposez de nombreux contextes de threads matériels, ces implémentations parallèles peuvent vous permettre d'améliorer considérablement les performances. Je n'ai utilisé que l'implémentation parallèle de sort mais pour donner une idée approximative, j'ai réussi à réduire le temps de tri de 14 à 4 secondes dans une de mes applications (environnement de test : vecteur de 100 millions d'objets avec fonction de comparaison personnalisée et machine à 8 cœurs).

Trucs et astuces supplémentaires

Contrairement aux sections des points précédents, cette partie ne nécessitent quelques petits changements dans le code . Ils sont aussi spécifiques à GCC (certains fonctionnent aussi avec Clang), donc les macros de compilation doivent être utilisées pour garder le code portable sur d'autres compilateurs. Cette section contient des techniques plus avancées, et ne devrait pas être utilisée si vous n'avez pas une certaine compréhension au niveau de l'assemblage de ce qui se passe. Notez également que les processeurs et les compilateurs sont assez intelligents de nos jours, il peut donc être difficile d'obtenir un avantage notable des fonctions décrites ici.

  • GCC builtins, qui sont listés aquí . Des constructions telles que __builtin_expect peut aider le compilateur à faire de meilleures optimisations en lui fournissant prédiction de branche informations. D'autres constructions telles que __builtin_prefetch apporte les données dans un cache avant qu'elles ne soient accédées et peut aider à réduire manques dans le cache .
  • les attributs de la fonction, qui sont énumérés aquí . En particulier, vous devriez examiner le hot y cold le premier indique au compilateur que la fonction est une fonction hotspot du programme et optimisera la fonction de manière plus agressive et la placera dans une sous-section spéciale de la section texte, pour une meilleure localité ; le dernier optimisera la fonction pour la taille et la placera dans une autre sous-section spéciale de la section texte.

J'espère que cette réponse sera utile à certains développeurs, et je serai heureux de prendre en compte toute modification ou suggestion.

18voto

Mikael Persson Points 7174

matériel relativement récent (Intel i5 ou i7)

Pourquoi ne pas investir dans un exemplaire de la Compilateur Intel et des bibliothèques de haute performance ? Il peut surpasser GCC en matière d'optimisations par une marge significative, typiquement de 10% à 30% ou même plus, et encore plus pour les programmes lourds de "number-crunching". Intel fournit également un certain nombre d'extensions et de bibliothèques pour les applications (parallèles) de traitement des nombres à haute performance, si vous pouvez vous permettre de les intégrer dans votre code. Cela peut s'avérer très rentable si vous gagnez des mois de temps d'exécution.

Nous avons déjà fait de notre mieux pour optimiser la source à la limite de nos compétences en programmation.

D'après mon expérience, le type de micro- et nano-optimisations que vous effectuez généralement à l'aide d'un profileur a tendance à avoir un faible retour sur investissement en temps par rapport aux macro-optimisations (rationalisation de la structure du code) et, plus important encore et souvent négligé, aux optimisations de l'accès à la mémoire (par exemple, la localité de la référence, la traversée de l'ordre, la minimisation de l'indirection, l'élimination des erreurs de cache, etc.) ). Ces dernières consistent généralement à concevoir les structures de la mémoire de manière à mieux refléter la façon dont la mémoire est utilisée (traversée). Parfois, il peut être aussi simple que de changer de type de conteneur et d'obtenir un énorme gain de performance grâce à cela. Souvent, avec les profileurs, on se perd dans les détails des optimisations instruction par instruction, et les problèmes de disposition de la mémoire n'apparaissent pas et sont généralement manqués lorsqu'on oublie de regarder la situation dans son ensemble. C'est une bien meilleure façon d'investir votre temps, et les bénéfices peuvent être énormes (par exemple, de nombreux algorithmes O(logN) finissent par être presque aussi lents que O(N) simplement à cause d'une mauvaise disposition de la mémoire (par exemple, l'utilisation d'une liste ou d'un arbre lié est un coupable typique d'énormes problèmes de performance par rapport à une stratégie de stockage contigu)).

8voto

zaufi Points 1837

Huh, alors la dernière chose que vous pouvez essayer : ACOVEA projet : Analysis of Compiler Optimizations via an Evolutionary Algorithm -- comme il ressort de la description, il s'agit d'utiliser un algorithme génétique pour choisir les meilleures options de compilation pour votre projet (en faisant la compilation plusieurs fois et en vérifiant le timing, en donnant un feedback à l'algorithme :). -- mais les résultats peuvent être impressionnants :)

8voto

Red XIII Points 1247

Si vous pouvez vous le permettre, essayez VTune . Il fournit BEAUCOUP plus d'informations que le simple échantillonnage (fourni par gprof, pour autant que je sache). Vous pourriez donner à la Analyste de code un essai. Ce dernier est un bon logiciel gratuit, mais il peut ne pas fonctionner correctement (ou pas du tout) avec les processeurs Intel.

Équipé d'un tel outil, il vous permet de vérifier diverses mesures telles que l'utilisation de la mémoire cache (et, fondamentalement, la disposition de la mémoire), qui - si elle est utilisée au maximum - donne un énorme coup de pouce à l'efficacité.

Lorsque vous êtes sûr que vos algorithmes et structures sont optimaux, vous devez absolument utiliser les cœurs multiples sur i5 et i7. En d'autres termes, jouez avec différents algorithmes et modèles de programmation parallèle et voyez si vous pouvez obtenir un gain de vitesse.

Lorsque vous disposez de données réellement parallèles (structures de type tableau sur lesquelles vous effectuez des opérations similaires ou identiques), vous devez donner à OpenCL et à l Instructions SIMD (plus facile à mettre en place).

4voto

user3708067 Points 116

Quelques notes sur la réponse actuellement choisie (je n'ai pas encore assez de points de réputation pour poster ceci en tant que commentaire) :

La réponse dit :

-fassociative-math , -freciprocal-math , -fno-signed-zeros y -fno-trapping-math . Ceux-ci ne sont pas inclus dans -Ofast et peut donner quelques gains de performance supplémentaires sur les calculs

Peut-être que c'était vrai lorsque la réponse a été postée, mais la Documentation GCC dit que tout cela est rendu possible par -funsafe-math-optimizations qui est activé par -ffast-math qui est activé par -Ofast . Cela peut être vérifié avec la commande gcc -c -Q -Ofast --help=optimizer qui montre quelles optimisations sont activées par -Ofast et confirme que tous ces éléments sont activés.

La réponse dit aussi :

d'autres drapeaux d'optimisation qui ne sont pas activés par les options "-O"... -frename-registers

Encore une fois, la commande ci-dessus montre que, au moins avec mon GCC 5.4.0, -frename-registers est activé par défaut avec -Ofast .

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