Optimisation 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 à la limite de nos compétences en programmation, y compris le profilage avec Gprof et Valgrind .

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

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

La question

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

Nous sums également intéressés par une optimisation mineure coûteuse, qui sera rentable à long terme.

Ce que nous utilisons maintenant

En ce moment, nous utilisons les options d’optimisation g ++ suivantes:

  • -Ofast : Niveau d’optimisation “standard” le plus élevé. Les -ffast-math inclus ne -ffast-math aucun problème dans nos calculs, nous avons donc décidé de nous lancer, malgré la non-conformité à la norme.
  • -march=native : -march=native l’utilisation de toutes les instructions spécifiques au processeur.
  • -flto pour permettre l’optimisation du temps de liaison, à travers différentes unités de compilation.

La plupart des réponses suggèrent des solutions alternatives, telles que différents compilateurs ou bibliothèques externes, qui apporteraient très probablement beaucoup de travail de réécriture ou d’intégration. Je vais essayer de coller à la question posée et me concentrer sur ce qui peut être fait avec GCC seul, en activant les indicateurs du compilateur ou en apportant des modifications minimes au code, comme demandé par l’OP. Ce n’est pas une réponse “vous devez faire ceci”, mais plutôt une collection de modifications 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 d’origine

Avant d’entrer dans les détails, quelques avertissements concernant la question, généralement pour les personnes qui viendront, liront la question et diront: “le PO optimise au-delà de l’O3, je devrais utiliser les mêmes drapeaux que lui!”.

  • -march=native permet d’utiliser des instructions spécifiques à une architecture de processeur 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 avec un processeur différent, ou être significativement plus lent (car cela active également mtune=native ), alors soyez conscient de cela si vous décidez de l’utiliser. Plus d’informations ici .
  • -Ofast vous l’avez dit, la -Ofast permet des optimisations non conformes aux normes , et doit donc être utilisée avec prudence. Plus d’informations ici .

Autres drapeaux GCC à essayer

Les détails des différents indicateurs sont répertoriés ici .

  • -Ofast active -ffast-math , ce qui permet à -fno-math-errno , -funsafe-math-optimizations , -ffinite-math-only , -fno-rounding-math , -fno-signaling-nans et -fcx-limited-range . Vous pouvez aller encore plus loin dans les optimisations de calcul à virgule flottante en ajoutant de manière sélective des indicateurs supplémentaires tels que -fno-signed-zeros , -fno-trapping-math et autres. Celles-ci ne sont pas incluses dans -Ofast et peuvent donner lieu à des augmentations de performances supplémentaires sur les calculs, mais vous devez vérifier si elles vous sont réellement utiles et ne casser aucun calcul.
  • GCC dispose également d’une grande quantité d’ autres indicateurs d’optimisation qui ne sont pas activés par les options “-O”. Ils sont listés comme “des options expérimentales pouvant produire un code cassé”. Encore une fois, ils doivent être utilisés avec précaution, et leurs effets vérifiés à la fois en testant l’exactitude et l’parsing comparative. Néanmoins, j’utilise souvent -frename-registers , cette option n’a jamais produit de résultats indésirables pour moi et tend à augmenter sensiblement les performances (c.-à-d. -frename-registers peut être mesurée lors de l’parsing comparative). C’est le type d’indicateur qui dépend beaucoup de votre processeur. -funroll-loops donne aussi parfois de bons résultats (et implique également -frename-registers ), mais cela dépend de votre code actuel.

PGO

GCC dispose de fonctionnalités d’ optimisation guidée par profil . Il n’y a pas beaucoup de documentation précise sur GCC à ce sujet, mais néanmoins, le faire fonctionner est assez simple.

  • comstackz d’abord votre programme avec -fprofile-generate .
  • laissez le programme s’exécuter (le temps d’exécution sera considérablement plus lent car le code génère également des informations de profil dans les fichiers .gcda).
  • recomstackr le programme avec -fprofile-use . Si votre application est multi-thread, ajoutez également l’ -fprofile-correction .

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

Mode parallèle de GCC

GCC dispose d’un mode parallèle , qui a été publié pour la première fois à l’époque où le compilateur GCC 4.2 était sorti.

Fondamentalement, il vous fournit des implémentations parallèles de nombreux algorithmes de la bibliothèque standard C ++ . Pour les activer globalement, il vous suffit d’append les -fopenmp et -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 peuvent être trouvées ici .

Si vous utilisez fréquemment ces algorithmes sur des structures de données volumineuses et que de nombreux contextes de threads matériels sont disponibles, ces implémentations parallèles peuvent vous donner un énorme gain de performances. Je n’ai utilisé que l’implémentation parallèle du sort jusqu’ici, mais pour donner une idée générale, j’ai réussi à réduire le temps de sorting de 14 à 4 secondes dans l’une de mes applications (environnement de test: vecteur de 100 millions d’objects avec comparateur personnalisé) fonction et machine à 8 cœurs).

Astuces supplémentaires

Contrairement aux sections précédentes, cette partie nécessite quelques modifications mineures dans le code . Ils sont aussi spécifiques à GCC (certains fonctionnent également sur 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 ne comprenez pas bien ce qui se passe au niveau de l’assemblage. Notez également que les processeurs et les compilateurs sont assez intelligents de nos jours, il peut donc être difficile d’obtenir des avantages visibles grâce aux fonctions décrites ici.

  • GCC incorporés, qui sont listés ici . Des constructions telles que __builtin_expect peuvent aider le compilateur à optimiser ses optimisations en lui fournissant des informations de prédiction de twig . D’autres constructions telles que __builtin_prefetch amènent les données dans un cache avant d’y accéder et peuvent aider à réduire les échecs du cache .
  • atsortingbuts de fonction, qui sont listés ici . En particulier, vous devriez examiner les atsortingbuts hot et cold ; le premier indiquera au compilateur que la fonction est un point chaud du programme et optimise la fonction de manière plus agressive et la place dans une sous-section spéciale de la section de texte, pour une meilleure localisation; la dernière optimisera la fonction pour la taille et la placera dans une autre sous-section spéciale de la section de texte.

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

matériel relativement nouveau (Intel i5 ou i7)

Pourquoi ne pas investir dans une copie du compilateur Intel et des bibliothèques hautes performances? Il peut surpasser de manière significative les optimisations GCC sur les optimisations, généralement de 10% à 30% ou même plus, et encore plus pour les programmes exigeants. Et Intel fournit également un certain nombre d’extensions et de bibliothèques pour des applications hautes performances (parallèles), si vous pouvez vous le permettre. Cela pourrait vous rapporter gros si cela vous fait économiser 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

Selon mon expérience, le type de micro et nano-optimisations que vous effectuez généralement avec l’aide d’un profileur a tendance à avoir un faible rendement des investissements en temps par rapport aux macro-optimisations (rationalisation de la structure du code) et surtout et souvent négligées, les optimisations d’access à la mémoire (par exemple, la localité de référence, la traversée dans l’ordre, la réduction de l’indirection, l’exclusion de la mémoire cache, etc.). Ce dernier implique généralement la conception des structures de mémoire pour mieux refléter la manière dont la mémoire est utilisée (traversée). Parfois, cela peut être aussi simple que de changer de type de conteneur et d’obtenir un gain de performance considérable. Souvent, avec les profileurs, vous vous perdez dans les détails des optimisations d’instructions, et les problèmes de disposition de la mémoire n’apparaissent pas et sont généralement ignorés lorsque vous oubliez de regarder la vue d’ensemble. C’est une bien meilleure façon d’investir votre temps, et les gains peuvent être énormes (par exemple, beaucoup d’algorithmes O (logN) finissent par être presque aussi lents que O (N) simplement à cause de la faible configuration de la mémoire (par exemple, utilisation d’une liste liée). ou arbre lié est un coupable typique d’énormes problèmes de performance par rapport à une stratégie de stockage contigu)).

Si vous pouvez vous le permettre, essayez VTune . Il fournit BEAUCOUP plus d’informations qu’un simple échantillonnage (fourni par gprof, pour autant que je sache). Vous pouvez essayer le code analyste . Latter est un logiciel décent et gratuit, mais il peut ne pas fonctionner correctement (ou pas du tout) avec les processeurs Intel.

Equipé d’un tel outil, il vous permet de vérifier diverses mesures, telles que l’utilisation du cache (et essentiellement la disposition de la mémoire), qui, si elle est utilisée dans son intégralité, améliore considérablement 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 / modèles de programmation en parallèle et voyez si vous pouvez accélérer.

Lorsque vous disposez de données réellement parallèles (structures de type tableau sur lesquelles vous effectuez des opérations similaires / identiques), vous devez essayer les instructions OpenCL et SIMD (plus faciles à configurer).

heh, alors vous pouvez essayer la dernière chose: Projet ACOVEA : Analyse des optimisations du compilateur via un algorithme évolutif – comme le montre la description, il essaie un algorithme génétique pour choisir les meilleures options de compilateur pour votre projet chronométrer, donner un retour à l’algorithme 🙂 – mais les résultats pourraient être impressionnants! 🙂

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

La réponse dit:

-fassociative-math , -freciprocal-math , -fno-signed-zeros et -fno-trapping-math . Ceux-ci ne sont pas inclus dans -Ofast et peuvent donner des augmentations de performances supplémentaires sur les calculs

Cela était peut-être vrai lorsque la réponse était postée, mais la documentation de GCC indique que tous ces éléments sont activés par l’ -funsafe-math-optimizations , qui est activée par -ffast-math , qui est activée 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 sont activés.

La réponse dit aussi:

autres indicateurs d’optimisation qui ne sont activés par aucune option “-O” … -frename-registers

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

Il est difficile de répondre sans plus de détails:

  • quel type de numérotation?
  • Quelles bibliothèques utilisez-vous?
  • quel degré de parallélisation?

Pouvez-vous écrire la partie de votre code qui prend le plus de temps? (Typiquement une boucle serrée)

Si vous êtes lié au CPU, la réponse sera différente de si vous êtes lié par IO.

Encore une fois, veuillez fournir plus de détails.

Je recommande de prendre en compte le type d’opération qui coûte cher et de rechercher une bibliothèque optimisée. Il y a beaucoup de bibliothèques vectorielles SIMD rapides, optimisées pour l’assemblage, pour les problèmes courants (principalement les mathématiques). Réinventer la roue est souvent tentant, mais cela ne vaut généralement pas la peine si une solution existante peut couvrir vos besoins. Puisque vous n’avez pas précisé le type de simulation, je ne peux que donner quelques exemples.

http: //www.yeppp.info/

http://eigen.tuxfamily.org/index.php?title=Main_Page

https://github.com/xianyi/OpenBLAS

avec gcc intel tour de / implement -fno-gcse (fonctionne bien sur gfortran) et -fno-guess-branch-prbability (par défaut dans gfortran)