Cet article vise à mettre en lumière la puissance des optimisations des compilateurs, en se concentrant sur les compilateurs Intel C++ , réputés pour leur popularité et leur utilisation généralisée.
Points forts : Que sont les optimisations du compilateur ? | -On | Architecture ciblée | Optimisation interprocédurale | -fno-alias | Rapports d'optimisation du compilateur
Tout compilateur exécute une série d'étapes pour convertir le code source de haut niveau en code machine de bas niveau. Celles-ci impliquent l'analyse lexicale, l'analyse syntaxique, l'analyse sémantique, la génération de code intermédiaire (ou IR), l'optimisation et la génération de code.
Pendant la phase d'optimisation, le compilateur recherche méticuleusement des moyens de transformer un programme, en visant une sortie sémantiquement équivalente qui utilise moins de ressources ou s'exécute plus rapidement. Les techniques utilisées dans ce processus englobent, sans toutefois s'y limiter , le repliement constant, l'optimisation des boucles, l'intégration de fonctions et l'élimination du code mort .
Les développeurs peuvent spécifier un ensemble d'indicateurs du compilateur pendant le processus de compilation, une pratique familière à ceux qui utilisent des options telles que « -g » ou « -pg » avec GCC pour les informations de débogage et de profilage. Au fur et à mesure, nous discuterons des indicateurs de compilateur similaires que nous pouvons utiliser lors de la compilation de notre application avec le compilateur Intel C++. Ceux-ci peuvent vous aider à améliorer l’efficacité et les performances de votre code.
u(x,y,t) est la température au point (x,y) au temps t.
Nous disposons essentiellement d'un codage C++ effectuant les itérations de Jacobi sur des grilles de tailles variables (que nous appelons résolutions). Fondamentalement, une taille de grille de 500 signifie résoudre une matrice de taille 500x500, et ainsi de suite.
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
MFLOP/s signifie « Millions d'opérations à virgule flottante par seconde ». Il s'agit d'une unité de mesure utilisée pour quantifier les performances d'un ordinateur ou d'un processeur en termes d'opérations en virgule flottante. Les opérations à virgule flottante impliquent des calculs mathématiques avec des nombres décimaux ou réels représentés dans un format à virgule flottante.
Note 1 : Pour fournir un résultat stable, j'exécute l'exécutable 5 fois pour chaque résolution et prends la valeur moyenne des valeurs MFLOP/s.
Remarque 2 : Il est important de noter que l'optimisation par défaut sur le compilateur Intel C++ est -O2. Il est donc important de spécifier -O0 lors de la compilation du code source.
Ce sont quelques-uns des indicateurs du compilateur les plus couramment utilisés lorsque l’on commence par les optimisations du compilateur. Dans un cas idéal, les performances de Ofast > O3 > O2 > O1 > O0 . Cependant, cela n'arrive pas nécessairement. Les points critiques de ces options sont les suivants :
-O1 :
-O2 :
-O3 :
-Derapide :
Il est bien évident que toutes ces optimisations sont bien plus rapides que notre code de base (avec « -O0 »). Le temps d’exécution est 2 à 3 fois inférieur au scénario de base. Qu'en est-il des MFLOP/s ??
Dans l’ensemble, bien que légèrement, « -O3 » est le plus performant.
Les indicateurs supplémentaires utilisés par « - Ofast » (« -no-prec-div -fp-model fast=2 ») n'apportent aucune accélération supplémentaire.
La réponse réside dans les indicateurs stratégiques du compilateur. Expérimenter des options telles que « -xHost » et, plus précisément, « -xCORE-AVX512 » peut nous permettre d'exploiter tout le potentiel des capacités de la machine et d'adapter les optimisations pour des performances optimales.
-xHôte :
-xCORE-AVX512 :
Objectif : demander explicitement au compilateur de générer du code qui utilise le jeu d'instructions Intel Advanced Vector Extensions 512 (AVX-512).
Caractéristiques principales : AVX-512 est un jeu d'instructions SIMD (Single Instruction, Multiple Data) avancé qui offre des registres vectoriels plus larges et des opérations supplémentaires par rapport aux versions précédentes comme AVX2. L'activation de cet indicateur permet au compilateur d'exploiter ces fonctionnalités avancées pour des performances optimisées.
Considérations : La portabilité est encore une fois le coupable ici. Les binaires générés avec les instructions AVX-512 peuvent ne pas fonctionner de manière optimale sur les processeurs qui ne prennent pas en charge ce jeu d'instructions. Ils ne fonctionneront peut-être pas du tout !
Par défaut, « -xCORE-AVX512 » suppose que le programme ne bénéficiera probablement pas de l'utilisation des registres zmm. Le compilateur évite d'utiliser les registres zmm sauf si un gain de performances est garanti.
Si l'on envisage d'utiliser les registres zmm sans restrictions, « » peut être réglé sur élevé. C'est ce que nous ferons également.
Waouh !
Ce qui est remarquable, c'est que nous avons obtenu ces résultats sans aucune intervention manuelle substantielle, simplement en incorporant une poignée d'indicateurs du compilateur pendant le processus de compilation de l'application.
Remarque : Ne vous inquiétez pas si votre matériel ne prend pas en charge AVX-512. Le compilateur Intel C++ prend en charge les optimisations pour AVX, AVX-2 et même SSE. La contient tout ce que vous devez savoir !
L’IPO est un processus en plusieurs étapes axé sur les interactions entre différentes fonctions ou procédures au sein d’un programme. L'introduction en bourse peut inclure de nombreux types d'optimisations différents, notamment la substitution directe, la conversion d'appel indirect et l'inlining.
-IPO :
Objectif : permet l'optimisation interprocédurale, permettant au compilateur d'analyser et d'optimiser l'ensemble du programme, au-delà des fichiers sources individuels, lors de la compilation.
Caractéristiques principales : - Optimisation de l'ensemble du programme : « -ipo » effectue une analyse et une optimisation sur tous les fichiers sources, en tenant compte des interactions entre les fonctions et les procédures tout au long du programme.- Optimisation inter-fonctions et inter-modules : l'indicateur facilite les fonctions en ligne et la synchronisation d'optimisations et d'analyse des flux de données dans différentes parties du programme.
Considérations : Cela nécessite une étape de liaison distincte. Après avoir compilé avec « -ipo », une étape de liaison particulière est nécessaire pour générer l'exécutable final. Le compilateur effectue des optimisations supplémentaires basées sur l'ensemble de la vue du programme lors de la liaison.
-ip :
Objectif : permet l'analyse-propagation interprocédurale, permettant au compilateur d'effectuer certaines optimisations interprocédurales sans nécessiter une étape de liaison distincte.
Caractéristiques principales : - Analyse et propagation : « -ip » permet au compilateur d'effectuer des recherches et de propager des données entre différentes fonctions et modules lors de la compilation. Cependant, il n'effectue pas toutes les optimisations qui nécessitent une vue complète du programme. - Compilation plus rapide : contrairement à « -ipo », « -ip » ne nécessite pas d'étape de liaison distincte, ce qui entraîne des temps de compilation plus rapides. Cela peut être bénéfique pendant le développement lorsqu’un retour d’information rapide est essentiel.
Considérations : Seules quelques optimisations interprocédurales limitées se produisent, y compris l'inlining de fonctions.
-ipo fournit généralement des capacités d'optimisation interprocédurales plus étendues car cela implique une étape de liaison distincte, mais cela se fait au prix de temps de compilation plus longs. [ ] -ip est une alternative plus rapide qui effectue certaines optimisations interprocédurales sans nécessiter une étape de liaison distincte, ce qui la rend adaptée aux phases de développement et de test.[ ]
Puisque nous ne parlons que des performances et des différentes optimisations, les temps de compilation ou la taille de l'exécutable ne nous préoccupent pas, nous nous concentrerons sur « -ipo ».
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
La fonction jacobi() prend quelques pointeurs pour servir de paramètres, puis fait quelque chose à l'intérieur des boucles for imbriquées. Lorsqu'un compilateur voit cette fonction dans le fichier source, il doit être très prudent.
L'expression pour calculer unew en utilisant u implique la moyenne de 4 valeurs u voisines. Et si vous et unew pointiez vers le même endroit ? Cela deviendrait le problème classique des pointeurs aliasés [ ].
Les compilateurs modernes sont très intelligents et, pour garantir la sécurité, ils supposent que l'alias est possible. Et pour des scénarios comme celui-ci, ils évitent toute optimisation pouvant avoir un impact sur la sémantique et la sortie du code.
Dans notre cas, nous savons que u et unew sont des emplacements mémoire différents et sont destinés à stocker des valeurs différentes. Ainsi, nous pouvons facilement faire savoir au compilateur qu’il n’y aura pas d’alias ici.
Il existe deux méthodes. Le premier est le mot - clé C « » . Mais cela nécessite de changer le code. Nous ne voulons pas de cela pour l'instant.
Quelque chose de simple ? Essayons « -fno-alias ».
-fno-alias :
Objectif : demander au compilateur de ne pas supposer d'alias dans le programme.
Caractéristiques principales : en supposant qu'il n'y ait pas d'alias, le compilateur peut optimiser plus librement le code, améliorant potentiellement les performances.
Considérations : Le développeur doit être prudent lorsqu'il utilise cet indicateur, car en cas d'alias injustifié, le programme peut donner des résultats inattendus.
Eh bien, maintenant nous avons quelque chose !!!
Un examen plus approfondi du code assembleur (bien qu'il ne soit pas partagé ici) et du rapport d'optimisation de compilation généré (voir ci-dessous ) révèle l'application astucieuse du compilateur en et . Ces transformations contribuent à des performances hautement optimisées, démontrant l'impact significatif des directives du compilateur sur l'efficacité du code.
Le compilateur Intel C++ fournit une fonctionnalité précieuse qui permet aux utilisateurs de générer un rapport d'optimisation résumant tous les ajustements effectués à des fins d'optimisation [ ]. Ce rapport complet est enregistré au format de fichier YAML, présentant une liste détaillée des optimisations appliquées par le compilateur dans le code. Pour une description détaillée, consultez la documentation officielle sur « ».
De même, les compilateurs Intel C++ (et tous les plus populaires) prennent également en charge les directives pragma, qui sont des fonctionnalités très intéressantes. Cela vaut la peine de vérifier certains pragmas comme ivdep, parallel, simd, vector, etc., sur la .