Cet article a fait l'objet d'une traduction automatique.

Compilateurs

Ce que tout programmeur devrait savoir sur les optimisations de compilateur

Hadi Brais

Téléchargez l'exemple de Code

Les langages de programmation haut niveau offrent plusieurs constructions de programmation abstraites telles que les fonctions, les instructions conditionnelles et les boucles qui nous rendent incroyablement productif. Cependant, un inconvénient d'écrire du code dans un langage de programmation haut niveau est la baisse potentiellement significative de performances. Idéalement, vous devez écrire un code compréhensible et facile à gérer, sans compromettre les performances. Pour cette raison, compilateurs tentent d'optimiser automatiquement le code pour améliorer ses performances, et ils sont devenus très sophistiqués faisant de nos jours. Ils peuvent transformer des boucles, des instructions conditionnelles et les fonctions récursives ; éliminer des blocs entiers de code ; et profitez de l'architecture cible du jeu d'instructions (ISA) pour rendre le code compact et rapide. Il est préférable de se concentrer sur l'écriture du code compréhensible, que de faire des optimisations manuelles qui résultent dans code énigmatique, difficile à gérer. En fait, manuellement optimiser le code pourrait empêcher le compilateur d'exécuter des optimisations supplémentaires ou plus efficaces.

Plutôt que manuellement optimisation de code, vous devez envisager les aspects de votre conception, par exemple en utilisant des algorithmes plus rapides, incorporant le parallélisme au niveau des threads et l'utilisation de fonctionnalités spécifiques à l'infrastructure (par exemple à l'aide de constructeurs de déménagement).

Cet article parle des optimisations du compilateur Visual C++. Je vais discuter des techniques d'optimisation les plus importants et les décisions qu'un compilateur doit faire afin de les appliquer. Le but n'est pas pour vous dire comment faire pour optimiser manuellement le code, mais pour vous montrer pourquoi vous pouvez faire confiance au compilateur d'optimiser le code pour votre compte. Cet article n'est en aucun cas un examen complet des optimisations effectuées par le compilateur Visual C++. Cependant, il montre les optimisations que vous voulez vraiment savoir sur et comment communiquer avec le compilateur de les appliquer.

Autres optimisations importantes qui dépassent actuellement les capacités de n'importe quel compilateur — par exemple, remplacer un algorithme inefficace par un efficace, ou changer la disposition d'une structure de données pour améliorer sa localité. Toutefois, ces optimisations débordent le cadre de cet article.

Définir les optimisations du compilateur

Une optimisation est le processus transformant un morceau de code dans un autre élément fonctionnellement équivalent du code aux fins d'améliorer un ou plusieurs de ses caractéristiques. Les deux caractéristiques les plus importantes sont la vitesse et la taille du code. Autres caractéristiques comprennent la quantité d'énergie nécessaire pour exécuter le code, le temps qu'il faut pour compiler le code et, dans le cas où le code résultant exige la compilation juste-à-temps (JIT), le temps nécessaire à JIT compiler le code.

Les compilateurs sont en constante amélioration en ce qui concerne les techniques qu'ils utilisent pour optimiser le code. Toutefois, ils ne sont pas parfaits. Pourtant, au lieu de passer du temps à peaufiner manuellement un programme, il est généralement beaucoup plus fructueuse pour utiliser les fonctionnalités spécifiques fournies par le compilateur et laissez le compilateur à modifier le code.

Il existe quatre façons d'aider le compilateur optimiser votre code plus efficacement :

  1. Écrire du code compréhensible et facile à gérer. Don' t regarder les fonctionnalités orientées objet de Visual C++, comme les ennemis de la performance. La dernière version de Visual C++ peut aussi réduire au minimum et parfois complètement éliminer.
  2. Utilisez les directives du compilateur. Par exemple, indiquer au compilateur d'utiliser une convention d'appel de fonction qui est plus rapide que celui par défaut.
  3. Utilisez les fonctions intrinsèques du compilateur. Une fonction intrinsèque est une fonction spéciale dont l'implémentation est fournie automatiquement par le compilateur. Le compilateur a une connaissance intime de la fonction et remplace l'appel de fonction avec une séquence très efficace d'instructions qui tirent parti de la cible ISA. Actuellement, Microsoft .NET Framework ne supporte pas les fonctions intrinsèques, alors aucune des langues gérées les soutenir. Toutefois, Visual C++ offre une prise en charge étendue pour cette fonctionnalité. Notez que tandis que l'utilisation des fonctions intrinsèques peut améliorer les performances du code, il réduit sa lisibilité et la portabilité.
  4. Utiliser l'optimisation guidée par profil (PGO). Avec cette technique, le compilateur sait plus d'infos sur comment le code va se comporter en cours d'exécution et peut l'optimiser en conséquence.

Le but de cet article est de vous montrer pourquoi vous pouvez faire confiance au compilateur en démontrant les optimisations effectuées sur code inefficace, mais compréhensible (application de la première méthode). Aussi, je vais donner une courte introduction à l'optimisation guidée par profil et mentionner certaines des directives du compilateur qui vous permettent d'affiner certaines parties de votre code.

Il existe de nombreuses techniques d'optimisation du compilateur allant de simples transformations comme repli constant, aux transformations extrêmes, tels que la planification de l'enseignement. Toutefois, dans cet article, je limiterai discussion à certaines des plus importantes optimisations — ceux qui peuvent considérablement améliorer les performances (d'un pourcentage à deux chiffres) et réduire le code taille : fonctionnalité inline, optimisations COMDAT et optimisations de la boucle. Je vais discuter les deux premiers dans la section suivante, puis montrer comment vous pouvez contrôler les optimisations effectuées par Visual C++. Enfin, je vais prendre un bref regard sur les optimisations dans le .NET Framework. Tout au long de cet article, je vais utiliser Visual Studio 2013 pour générer le code.

Génération de Code lien

Link-Time Code Generation (LTCG) est une technique pour exécuter des optimisations de l'ensemble du programme (WPO) sur du code C/C++. Le compilateur C/C++ compile chaque fichier source séparément et produit le fichier objet correspondant. Cela signifie que le compilateur ne peut appliquer des optimisations que sur un seul fichier source plutôt que sur l'ensemble du programme. Toutefois, certaines optimisations importantes peuvent être effectuées qu'en regardant l'ensemble du programme. Vous pouvez appliquer ces optimisations au moment de lien plutôt qu'au moment de la compilation car l'éditeur de liens a une vision complète du programme.

Lorsque /LTCG est activé (en spécifiant le commutateur de compilation/GL), le pilote de compilateur (cl.exe) va invoquer seulement l'extrémité avant du compilateur (c1.dll ou c1xx.dll) et reporter les travaux de la partie arrière (c2.dll) jusqu'au moment de lien. Les fichiers d'objet qui en résulte contient C Inter­médiateur code langue (CIL) plutôt que le code dépendant de la machine Assemblée. Puis, lorsque l'éditeur de liens (link.exe) est appelé, il voit que les fichiers contiennent du code CIL et appelle le back-end du compilateur, qui à son tour effectue WPO, génère les fichiers de l'objet binaire et retourne dans l'éditeur de liens pour assembler tous les fichiers objets et générer l'exécutable.

Le serveur frontal exécute certaines optimisations, telles que de repli constant, indépendamment de savoir si les optimisations sont activées ou désactivées. Cependant, toutes les optimisations importantes sont effectuées par le back-end du compilateur et peuvent être contrôlées à l'aide de commutateurs du compilateur.

/LTCG permet le back-end effectuer de nombreuses optimisations agressivement (en spécifiant/GL ainsi que les commutateurs du compilateur/O1 ou/O2 et /Gw et les commutateurs de l'éditeur de liens/OPT : REF et/opt : ICF). Dans cet article, j'aborderai uniquement la fonctionnalité inline et optimisations de COMDAT. Pour une liste complète des optimisations LTCG, reportez-vous à la documentation. Notez que l'éditeur de liens peut exécuter /LTCG sur fichiers objet natif, mélangés les fichiers objet natif/managé pur objet managé, fichiers d'objet managé sécurisé et fichiers .netmodule sécurisé.

Je créerai un programme constitué de deux fichiers sources (source1.c et source2.c) et un fichier d'en-tête (source2.h). Les fichiers source1.c et source2.c figurent dans Figure 1 et Figure 2, respectivement. Le fichier d'en-tête qui contient les prototypes de toutes les fonctions dans source2.c, est assez simple, donc je ne le montre ici.

Figure 1 la source1.c fichier

#include <stdio.h> // scanf_s and printf.
#include "Source2.h"
int square(int x) { return x*x; }
main() {
  int n = 5, m;
  scanf_s("%d", &m);
  printf("The square of %d is %d.", n, square(n));
  printf("The square of %d is %d.", m, square(m));
  printf("The cube of %d is %d.", n, cube(n));
  printf("The sum of %d is %d.", n, sum(n));
  printf("The sum of cubes of %d is %d.", n, sumOfCubes(n));
  printf("The %dth prime number is %d.", n, getPrime(n));
}

Figure 2 la source2.c fichier

#include <math.h> // sqrt.
#include <stdbool.h> // bool, true and false.
#include "Source2.h"
int cube(int x) { return x*x*x; }
int sum(int x) {
  int result = 0;
  for (int i = 1; i <= x; ++i) result += i;
  return result;
}
int sumOfCubes(int x) {
  int result = 0;
  for (int i = 1; i <= x; ++i) result += cube(i);
  return result;
}
static
bool isPrime(int x) {
  for (int i = 2; i <= (int)sqrt(x); ++i) {
    if (x % i == 0) return false;
  }
  return true;
}
int getPrime(int x) {
  int count = 0;
  int candidate = 2;
  while (count != x) {
    if (isPrime(candidate))
      ++count;
  }
  return candidate;
}

Le fichier source1.c contient deux fonctions : la fonction carrée, qui prend un entier et retourne à sa place, et la fonction principale du programme. La fonction appelle la fonction carrée et toutes les fonctions de source2.c sauf isPrime. Le fichier source2.c contient cinq fonctions : la fonction cube retourne le cube d'un entier donné ; la fonction sum retourne la somme de tous les entiers de 1 à un entier donné ; la fonction sumOfcubes renvoie la somme des cubes de tous les entiers de 1 à un entier donné ; la fonction isPrime détermine si un entier donné est premier ; et la fonction getPrime qui retourne le nombre de premier XE. J'ai omis parce qu'il n'est pas d'intérêt dans cet article de vérification des erreurs.

Le code est simple mais utile. Il y a un certain nombre de fonctions qui exécutent des calculs simples ; certains nécessitent simple pour les boucles. La fonction getPrime est la plus complexe, car il contient un certain temps en boucle et, dans la boucle, il appelle la fonction isPrime, qui contient également une boucle. Je vais utiliser ce code pour démontrer une des plus importantes optimisations du compilateur, à savoir la fonction Inline, ainsi que d'autres optimisations.

Je vais créer le code dans trois configurations différentes et examinez les résultats pour déterminer comment il a été transformé par le compilateur. Si vous suivez le long, vous aurez besoin du fichier de sortie assembleur (produit avec le commutateur de compilation/FA [s]) pour examiner le code résultant de l'Assemblée et le fichier de mappage (produit avec le commutateur de l'éditeur de liens /MAP) afin de déterminer les optimisations de COMDAT qui ont été exécutées (l'éditeur de liens peut le signaler également si vous utilisez le / verbose : icf et / verbose : Réf commutateurs). Donc, assurez-vous de que ces commutateurs sont spécifiés dans toutes les configurations suivantes que j'ai discuter. Aussi, je vais utiliser le compilateur C (/ TC) afin que le code généré est plus facile d'examiner. Cependant, tout ce que j'ai discuter ici s'applique également au code C++.

La Configuration Debug

La configuration de débogage est utilisée principalement parce que toutes les optimisations de dorsaux sont désactivées lorsque vous spécifiez le commutateur de compilateur /Od sans spécifier le commutateur/GL. Lorsque vous générez le code sous cette configuration, les fichiers objet contiendra le code binaire qui correspond exactement au code source. Vous pouvez examiner les fichiers de sortie assembleur qui en résulte et le fichier de mappage pour le confirmer. Cette configuration est équivalente à la configuration Debug du Visual Studio.

La Configuration de version de génération de Code de compilation

Cette configuration est similaire à la configuration Release dans les optimisations sont activées (en spécifiant les commutateurs de compilation/O1, / O2 ou /Ox), mais sans spécifier le commutateur de compilation/GL. Sous cette configuration, l'objet résultant contiendront des fichiers binaire un code optimisé. Toutefois, aucune optimisation au niveau de la totalité du programme n'est effectuées.

En examinant l'assembly généré liste fichier de source1.c, vous remarquerez que deux optimisations ont été effectuées. Tout d'abord, le premier appel à la place d'int, square(n), en Figure 1 a été complètement éliminée en évaluant le calcul au moment de la compilation. Comment est-ce arrivé ? Le compilateur a déterminé que la fonction carrée est petite, il devrait donc être inline. Après cette fonctionnalité, le compilateur a déterminé que la valeur de la variable locale n est connue et qu'elle ne change pas entre l'instruction d'assignation et l'appel de fonction. Il a donc conclu qu'il est sûr à exécuter la multiplication et à substituer le résultat (25). Dans l'optimisation du deuxième, le deuxième appel à la fonction carré, square(m), a été marquée comme Inline, aussi bien. Cependant, parce que la valeur de m n'est pas connue au moment de la compilation, le compilateur ne peut pas évaluer le calcul, donc le code réel est émis.

Maintenant, je vais examiner le fichier de liste d'assembly de source2.c, qui est beaucoup plus intéressant. L'appel à la fonction cube en sumOfCubes a été traité en mode inline. Cela a permis au compilateur d'effectuer des optimisations significatives sur la boucle (comme vous le verrez dans la section « Boucle optimisations »). En outre, le jeu d'instructions SSE2 est utilisé dans la fonction isPrime pour convertir int de doubler lorsque vous appelez la fonction sqrt et également pour convertir double en int lors du retour de sqrt. Et sqrt est appelée une seule fois avant le début de la boucle. Notez que si aucune /arch commutateur est spécifié à la compilation, le compilateur x 86 utilise SSE2 par défaut. Plus déployée x 86 processeurs, ainsi que tous les processeurs x 86-64, support SSE2.

La configuration Release /LTCG est identique à la configuration Release au Visual Studio. Dans cette configuration, les optimisations sont activées et le commutateur de compilateur/GL est spécifié. Ce commutateur est spécifié implicitement lors de l'utilisation/O1 ou/O2. Il indique au compilateur d'émettre des fichiers objets CIL plutôt que des fichiers d'objet assembly. De cette façon, l'éditeur de liens appelle le back-end du compilateur d'exécuter WPO comme décrit précédemment. Maintenant, j'aborderai plusieurs optimisations WPO pour montrer l'immense avantage de /LTCG. Les listes de code d'assembly générés avec cette configuration sont disponibles en ligne.

Tant la fonctionnalité inline est activée (/ Ob, qui est activée chaque fois que vous demandez des optimisations), le commutateur/GL permet au compilateur de fonctions inline définies dans d'autres unités de traduction indépendamment de savoir si le commutateur de compilateur /Gy (voir un peu plus loin) est spécifié. Le commutateur de l'éditeur de liens /LTCG est facultatif et fournit des directives pour l'éditeur de liens uniquement.

En examinant le listing assembleur du fichier de source1.c, vous pouvez voir que tous les appels de fonction à l'exception de scanf ont été Inline. En conséquence, le compilateur est capable d'exécuter les calculs du cube, somme et sumOfCubes. Seule la fonction isPrime n'a pas été traité en mode inline. Cependant, si on Inline manuellement dans getPrime, le compilateur aurait encore inline getPrime à main.

Comme vous pouvez le voir, fonctionnalité inline est importante non seulement parce qu'il optimise loin un appel de fonction, mais aussi parce qu'elle permet au compilateur d'effectuer de nombreuses autres optimisations en conséquence. L'inlining une fonction généralement améliore les performances au détriment de l'augmentation de la taille du code. Utilisation excessive de cette optimisation conduit à un phénomène connu comme code de ballonnement. Dans chaque site d'appel, le compilateur effectue une analyse coûts/bénéfices et décide ensuite s'il faut en ligne la fonction.

En raison de l'importance de la fonction Inline, le compilateur Visual C++ fournit un soutien beaucoup plus que ce que la norme dicte concernant le contrôle de cette fonctionnalité. Vous pouvez demander au compilateur de jamais inline un éventail de fonctions à l'aide du pragma d'auto_inline de façon. Vous pouvez indiquer au compilateur de jamais inline, une méthode ou une fonction spécifique, en le marquant avec noinline. Vous pouvez marquer une fonction avec le mot clé inline pour donner un indice pour le compilateur de ligne de la fonction (bien que le compilateur peut choisir d'ignorer cette astuce Si inlining serait une perte nette). Le mot clé inline est disponible depuis la première version de C++ — il a été introduit en C99. Vous pouvez utiliser le Microsoft spécifique mot clé __inline dans le code C et C++ ; Il est utile lorsque vous utilisez une ancienne version de C qui ne supporte pas ce mot clé. En outre, vous pouvez utiliser le mot-clé __forceinline (C et C++) pour forcer le compilateur à toujours inline une fonction lorsque cela est possible. Et enfin, et surtout, vous pouvez demander au compilateur de déplier une fonction récursive soit à une profondeur spécifique ou indéterminée par appliquer la fonctionnalité inline à l'aide du pragma inline_recursion. Notez que le compilateur n'offre actuellement aucuns fonctionnalités qui vous permettent de contrôler cette fonctionnalité dans le site d'appel plutôt qu'à la définition de fonction.

Le /Ob0 commutateur désactive cette fonctionnalité complètement, qui prend effet par défaut. Vous devez utiliser ce commutateur lorsque vous déboguez (c'est spécifié automatiquement dans la Configuration Debug Visual Studio ). Le commutateur/Ob1 indique au compilateur de ne tenir compte que des fonctions pour appliquer la fonctionnalité inline qui sont marqués avec inline, __inline ou __forceinline. Le commutateur/OB2, qui prend effet lorsque vous spécifiez/o [1|2|x], indique au compilateur de considérer n'importe quelle fonction pour cette fonctionnalité. À mon avis, la seule raison d'utiliser les mots clés inline ou __inline est de contrôler cette fonctionnalité avec le commutateur/ob1.

Le compilateur ne pourrez inline une fonction dans certaines conditions. Un exemple est lorsque vous appelez une fonction virtuelle pratiquement ; la fonction ne peut pas être inline car le compilateur ne sachent pas quelle fonction va être appelé. Autre exemple : lorsque vous appelez une fonction par un pointeur vers la fonction plutôt que d'utiliser son nom. Vous devez vous efforcer d'éviter de telles conditions pour permettre cette fonctionnalité. Reportez-vous à la documentation MSDN pour obtenir une liste complète de ces conditions.

Fonctionnalité inline n'est pas la seule optimisation qui est plus efficace­tive lorsqu'il est appliqué au niveau de l'ensemble du programme. En fait, la plupart des optimisations devient plus efficaces à ce niveau. Dans le reste de cet article, j'aborderai une classe spécifique de ces optimisations appelé optimisations COMDAT.

Par défaut, lors de la compilation d'une unité de traduction, tout le code se déposera dans une seule section dans le fichier objet. L'éditeur de liens fonctionne au niveau section. Autrement dit, il peut retirer les sections, combiner des sections et réorganiser des sections. Cela empêche l'éditeur de liens d'exécuter des optimisations de trois que peut significativement (pourcentage à deux chiffres) de réduire la taille de l'exécutable et améliorer ses performances. Le premier est éliminer les variables globales et fonctions non référencées. Le second se replie des fonctions identiques et constantes variables globales. Le troisième est réorganisation des fonctions et variables globales pour les fonctions qui tombent sur le même chemin d'exécution et les variables qui sont accessibles à l'ensemble sont physiquement situées plus près en mémoire afin d'améliorer la localité.

Pour activer ces optimisations de l'éditeur de liens, vous pouvez indiquer au compilateur d'empaqueter des fonctions et variables en sections distinctes en spécifiant le /Gy (liaison au niveau des fonctions) et les commutateurs de compilation /Gw (optimisation globale des données), respectivement. Ces sections sont appelées les COMDAT. Vous pouvez également marquer une variable de données global particulier avec __declspec (selectany) pour indiquer au compilateur pour emballer la variable dans un COMDAT. Puis, en spécifiant le commutateur de l'éditeur de liens/OPT : Ref, l'éditeur de liens éliminera les fonctions non référencées et variables globales. En outre, en spécifiant le commutateur / OPT : ICF, l'éditeur de liens se replient des fonctions identiques et variables constantes globales. (ICF signifie pliage de COMDAT identique). Avec le commutateur de l'éditeur de liens/Order, vous pouvez indiquer à l'éditeur de liens pour placer les COMDAT dans l'image qui en résulte dans un ordre spécifique. Notez que toutes ces optimisations sont des optimisations de l'éditeur de liens et ne nécessitent pas le commutateur de compilation/GL. Les commutateurs / OPT : ICF et / opt : Ref doivent être désactivés pendant le débogage pour des raisons évidentes.

Utilisez /LTCG lorsque cela est possible. La seule raison de ne pas utilisez /LTCG est lorsque vous souhaitez distribuer l'objet qui en résulte et les fichiers de la bibliothèque. Rappelons que ces fichiers contiennent des CIL code au lieu du code assembleur. Code CIL peut être consommée uniquement par le compilateur/éditeur de liens de la même version qui l'a produit, et qui peut limiter considérablement la facilité d'utilisation des fichiers objets parce que les développeurs doivent avoir la même version du compilateur à utiliser ces fichiers. Dans ce cas, sauf si vous êtes prêt à distribuer les fichiers objets pour chaque version du compilateur, vous devez utiliser génération de compilation de code à la place. En plus de la facilité d'utilisation limitée, ces fichiers objet sont plusieurs fois plus grands en taille que les fichiers d'objet assembleur correspondant. Cependant, garder à l'esprit l'avantage énorme de fichiers objets CIL, qui permet à WPO.

Optimisations de la boucle

Le compilateur Visual C++ prend en charge plusieurs optimisations de boucle, mais j'aborderai que trois : vectorisation déroulant, automatique de boucle et motion code invariant de boucle. Si vous modifiez le code dans Figure 1 sorte que m est passée à sumOfCubes au lieu de n, le compilateur ne sera pas en mesure de déterminer la valeur du paramètre, donc il doit compiler la fonction pour gérer n'importe quel argument. La fonction qui en résulte est optimisée et sa taille est assez grande, afin que le compilateur n'inline il.

La compilation du code avec les résultats de commutateur/O1 en code assembleur optimisé pour l'espace. Dans ce cas, aucune optimisation ne sera effectuée sur la fonction sumOfCubes. Compilation avec les résultats de commutateur/O2 dans le code qui est optimisé pour la vitesse. La taille du code sera beaucoup plus importante encore significativement plus vite parce que la boucle à l'intérieur de la sumOfCubes a été déroulée et vectorisée. Il est important de comprendre que vectorisation ne serait pas possible sans cette fonctionnalité la fonction cube. En outre, déroulement de la boucle ne serait pas qu'à compter sans cette fonctionnalité. Une représentation graphique simplifiée de la code de l'assembly résultant est montrée dans Figure 3. Le graphique de flux est la même chose pour les architectures x 86 et x 86-64.

contrôle graphique de flux de sumOfCubes
Figure 3 contrôle graphique de flux de sumOfCubes

Dans Figure 3, le diamant vert est le point d'entrée et les rectangles rouges sont les points de sortie. Les diamants bleus représentent les conditions qui sont en cours d'exécution dans le cadre de la fonction sumOfCubes en cours d'exécution. Si le SSE4 est pris en charge par le processeur et x est supérieure ou égale à huit, puis SSE4 instructions serviront à effectuer quatre multiplications en même temps. Le processus d'exécution de la même opération sur plusieurs valeurs simultanément s'appelle vectorisation. En outre, le compilateur va dérouler la boucle deux fois ; autrement dit, le corps de la boucle sera répété deux fois dans chaque itération. L'effet combiné est que huit multiplications seront effectuées pour chaque itération. Lorsque x devient inférieur à huit, les instructions traditionnelles sont utilisées pour exécuter le reste des calculs. Notez que le compilateur a émis trois points de sortie contenant les épilogues distincts dans la fonction au lieu d'un. Cela réduit le nombre de sauts.

Déroulement de la boucle est le processus de répétition du corps de la boucle dans la boucle afin que plus d'une itération de la boucle est exécutée au sein d'une seule itération de la boucle déroulée. Cela améliore les performances parce que les instructions de commande boucle seront exécutées moins fréquemment. Peut-être plus important encore, il pourrait permettre au compilateur d'effectuer de nombreuses autres optimisations, telles que de la vectorisation. L'inconvénient de déroulement est qu'elle augmente la taille du code et enregistrer la pression. Toutefois, selon le corps de la boucle, elle peut améliorer les performances d'un pourcentage à deux chiffres.

Contrairement à x 86 processeurs, tout soutien de processeurs x 86-64 SSE2. En outre, vous pouvez tirer parti des ensembles d'instructions AVX/AVX2 de la dernière microarchitectures x 86-64 d'Intel et AMD en spécifiant le /arch basculer. En spécifiant /arch:AVX2 permet au compilateur d'utiliser les FMA et IMC jeux d'instructions, aussi bien.

Actuellement, le compilateur Visual C++ n'activez vous permet de contrôler le déroulement de la boucle. Toutefois, vous pouvez émuler cette technique en utilisant des modèles avec le mot clé forceinline __. Vous pouvez désactiver l'auto-vectorisation sur une boucle spécifique à l'aide du pragma de boucle avec l'option no_vector.

En regardant le code assembleur généré, les yeux perçants remarquerait que le code peut être optimisé un peu plus. Toutefois, le compilateur a déjà fait un excellent travail et ne sera pas passer beaucoup plus de temps l'analyse du code et d'appliquer des optimisations mineures.

someOfCubes n'est pas la seule fonction dont boucle a été déroulé. Si vous modifiez le code afin que m est passé à la fonction sum au lieu de n, le compilateur ne sera pas en mesure d'évaluer la fonction et, par conséquent, il doit émettre son code. Dans ce cas, la boucle sera déroulée deux fois.

La dernière optimisation que j'aborderai est motion code invariant de boucle. Examiner le morceau de code suivant :

int sum(int x) {
  int result = 0;
  int count = 0;
  for (int i = 1; i <= x; ++i) {
    ++count;
    result += i;
  }
  printf("%d", count);
  return result;
}

Le seul changement ici, c'est que j'ai une variable supplémentaire qui est incrémenté à chaque itération et ensuite imprimé. Il n'est pas difficile de voir que ce code peut être optimisé en déplaçant l'incrémentation de la variable nombre en dehors de la boucle. Autrement dit, je peux juste assigner x à la variable count. Cette optimisation est appelée mouvement code invariant de boucle. La partie de l'invariant de boucle indique clairement que cette technique ne fonctionne que si le code ne dépend d'aucune des expressions dans l'en-tête de la boucle.

Maintenant, voici la capture : Si vous appliquez cette optimisation manuellement, le code qui en résulte peut-être présenter des performances dégradées dans certaines conditions. Vous pouvez voir pourquoi ? Envisagez ce qui se passe lorsque x est une. La boucle s'exécute jamais, ce qui signifie que dans la version non optimisée, la variable count ne sera pas être touchée. Comment­jamais, dans la version manuelle optimisée, une affectation inutile de x à compter est exécutée en dehors de la boucle ! En outre, si x est négatif, puis comte tiendrait une valeur incorrecte. Les humains et les compilateurs sont sensibles à ces écueils. Heureusement, le compilateur Visual C++ est assez intelligent pour comprendre cela en émettant la condition de la boucle avant la cession, ce qui entraîne une amélioration des performances pour toutes les valeurs de x.

En résumé, si vous n'êtes ni un compilateur, ni un expert, les optimisations du compilateur vous devriez éviter de faire des transformations manuelles à votre code juste pour rendre lui le sembler plus rapidement. Garder vos mains propres et faire confiance au compilateur pour optimiser votre code.

Contrôle les optimisations

Outre le compilateur commutateurs/O1, / O2 et/OX, vous pouvez contrôler les optimisations pour des fonctions spécifiques à l'aide du pragma optimize, qui ressemble à ceci :

#pragma optimize( "[optimization-list]", {on | off} )

La liste d'optimisation peut être vide ou contenir une ou plusieurs des valeurs suivantes : g, s, t et y. Ceux-ci correspondent aux commutateurs du compilateur /og., /Os, /Ot et/Oy, respectivement.

Une liste vide avec le paramètre arrêt provoque toutes ces optimisations d'éteindre indépendamment les commutateurs du compilateur qui ont été spécifiées. Une liste vide avec le paramètre sur provoque les commutateurs du compilateur spécifié soit prise en compte.

Le commutateur /Og active les optimisations globales, qui sont ceux qui peuvent être effectuées en regardant la fonction en cours d'optimisation uniquement, pas à toutes les fonctions qu'elle appelle. Si /LTCG est activé, /Og active WPO.

Le pragma optimize est utile lorsque vous souhaitez différentes fonctions de différentes façons — certains pour l'espace et d'autres pour la vitesse. Toutefois, si vous voulez vraiment avoir ce niveau de contrôle, vous devez envisager optimisation guidée par profil (PGO), qui est le processus permettant d'optimiser le code en utilisant un profil qui contient des informations comportementales enregistrées lors de l'exécution d'une version instrumentée du code. Le compilateur utilise le profil pour prendre de meilleures décisions sur la façon d'optimiser le code. Visual Studio fournit les outils nécessaires pour appliquer cette technique sur le code natif et managé.

Optimisations dans .NET

Il n'y a aucun linker impliquées dans le modèle de compilation .NET. Toutefois, il existe un compilateur de code source (du compilateur c#) et un compilateur JIT. Le compilateur de code source effectue uniquement des optimisations mineures. Par exemple, il n'est pas exécuter la fonctionnalité inline et optimisations de boucle. Au lieu de cela, ces optimisations sont gérées par le compilateur JIT. Le compilateur JIT qui est livré avec toutes les versions du .NET Framework jusqu'à 4,5 ne supporte pas les instructions SIMD. Toutefois, le compilateur JIT fourni avec .NET Framework 4.5.1 et versions ultérieures, appelées RyuJIT, prend en charge SIMD.

Quelle est la différence entre RyuJIT et Visual C++ en termes de capacités d'optimisation ? Parce qu'il fait son travail en cours d'exécution, RyuJIT peut exécuter des optimisations que Visual C++ ne peut pas. Par exemple, au moment de l'exécution, RyuJIT pourrait être en mesure de déterminer que la condition d'un if instruction n'est jamais vraie lors de cette exécution particulière de la demande et, par conséquent, il peut être optimisé loin. Aussi RyuJIT peut tirer parti des capacités du processeur sur lequel il s'exécute. Par exemple, si le processeur prend en charge SSE4.1, le compilateur JIT émet seulement instructions SSE4.1 pour la fonction sumOfcubes, rendant le code généré beaucoup plus compact. Toutefois, il ne peut dépenser beaucoup de temps optimiser le code, car le temps de compilation JIT un impact sur les performances de l'application. En revanche, le compilateur Visual C++ peut dépenser beaucoup plus de temps à repérer d'autres possibilités d'optimisation­affaires et profiter d'eux. Une grande nouvelle technologie de Microsoft, appelé .NET natives, permet de compiler du code managé dans des exécutables autonomes optimisés en utilisant le Visual C++ retour fin. Actuellement, cette technologie prend en charge uniquement les applications Windows Store.

La capacité de contrôler les optimisations de code managé est actuellement limitée. Les compilateurs c# et Visual Basic seulement fournissent la possibilité d'activer ou désactiver des optimisations à l'aide de la / optimiser l'interrupteur. Pour contrôler les optimisations JIT, vous pouvez appliquer la System.Runtime.Compiler­Services.MethodImpl attribut sur une méthode avec une option de MethodImplOptions spécifiées. L'option NoOptimization désactive les optimisations, l'option NoInlining empêche la méthode d'être inline et l'option AggressiveInlining (.NET 4.5) donne une recommandation (plus qu'un soupçon) au compilateur JIT inline de la méthode.

Jaquette en haut

Toutes les techniques d'optimisation abordées dans cet article peuvent améliorer considérablement les performances de votre code par un pourcentage à deux chiffres, et chacun d'eux sont pris en charge par le compilateur Visual C++. Ce qui rend ces techniques importants, c'est que, lorsqu'il est appliqué, ils permettent au compilateur d'effectuer d'autres optimisations. Cela n'est absolument pas une discussion complète des optimisations du compilateur interprété par Visual C++. Cependant, j'espère qu'elle vous a donné une appréciation des capacités du compilateur. Visual C++ peut faire plus, beaucoup plus, alors restez branchés pour la partie 2.


Hadi Brais est un Ph.d. chercheur à l'Institut indien de technologie Delhi (IITD), recherches sur les optimisations du compilateur pour la technologie de mémoire de nouvelle génération. Il passe le plus clair de son temps à l'écriture de code en C / C++ c++ / C# et creuser profondément dans le CLR et le CRT. Il blogs à hadibrais.wordpress.com. Le contacter au hadi.b@live.com.

Grâce à l'expert technique Microsoft suivant d'avoir relu cet article : Jim Hogg