Bonnes pratiques d’optimisation
Ce document décrit certaines bonnes pratiques pour optimiser les programmes C++ dans Visual Studio.
Options du compilateur et de l’éditeur de liens
Optimisation guidée par profil
Visual Studio prend en charge l’optimisation guidée par profil (PGO). Cette optimisation utilise des données de profil à partir d’exécutions d’apprentissage d’une version instrumentée d’une application pour piloter l’optimisation ultérieure de l’application. L’utilisation de PGO peut prendre du temps. Il peut donc ne pas s’agir d’un élément utilisé par chaque développeur, mais nous vous recommandons d’utiliser PGO pour la version finale d’un produit. Pour plus d’informations, consultez Optimisations guidées par profil.
En outre, l’optimisation complète du programme (également connu sous le nom de génération de code de temps de liaison) et les /O1
/O2
optimisations ont été améliorées. En général, une application compilée avec l’une de ces options sera plus rapide que la même application compilée avec un compilateur antérieur.
Pour plus d’informations, consultez (Optimisation complète du programme) et/O1
, /O2
(Réduire la taille, Agrandir la vitesse)./GL
Quel niveau d’optimisation utiliser
Si possible, les builds de version finale doivent être compilées avec des optimisations guidées par profil. S’il n’est pas possible de créer avec PGO, qu’il s’agisse d’une infrastructure insuffisante pour exécuter les builds instrumentées ou de ne pas avoir accès aux scénarios, nous vous suggérons de créer avec l’optimisation complète du programme.
Le /Gy
commutateur est également très utile. Il génère un COMDAT distinct pour chaque fonction, ce qui donne plus de flexibilité à l’éditeur de liens lorsqu’il s’agit de supprimer les COMDAT non référencés et le pliage COMDAT. Le seul inconvénient de l’utilisation /Gy
est qu’il peut provoquer des problèmes lors du débogage. Par conséquent, il est généralement recommandé de l’utiliser. Pour plus d’informations, consultez /Gy
(Activer la liaison au niveau de la fonction).
Pour la liaison dans les environnements 64 bits, il est recommandé d’utiliser l’option /OPT:REF,ICF
éditeur de liens et, dans les environnements 32 bits, /OPT:REF
il est recommandé. Pour plus d’informations, consultez /OPT (Optimisations).
Il est également fortement recommandé de générer des symboles de débogage, même avec des builds de mise en production optimisées. Il n’affecte pas le code généré et facilite le débogage de votre application, si nécessaire.
Commutateurs à virgule flottante
L’option /Op
du compilateur a été supprimée et les quatre options de compilateur suivantes traitant des optimisations à virgule flottante ont été ajoutées :
Option | Description |
---|---|
/fp:precise |
Il s’agit de la recommandation par défaut et doit être utilisée dans la plupart des cas. |
/fp:fast |
Recommandé si les performances sont de la plus grande importance, par exemple dans les jeux. Cela entraînera les performances les plus rapides. |
/fp:strict |
Recommandé si des exceptions à virgule flottante précises et le comportement IEEE sont souhaités. Cela entraînera des performances les plus lentes. |
/fp:except[-] |
Peut être utilisé conjointement avec /fp:strict ou /fp:precise , mais pas /fp:fast . |
Pour plus d’informations, consultez /fp
(Spécifier le comportement à virgule flottante).
Declspecs d’optimisation
Dans cette section, nous allons examiner deux declspecs qui peuvent être utilisés dans les programmes pour aider les performances : __declspec(restrict)
et __declspec(noalias)
.
Le restrict
declspec ne peut être appliqué qu’aux déclarations de fonction qui retournent un pointeur, par exemple __declspec(restrict) void *malloc(size_t size);
Le restrict
declspec est utilisé sur les fonctions qui retournent des pointeurs nonalias. Ce mot clé est utilisé pour l’implémentation de la bibliothèque C-Runtime, car il ne retourne jamais une valeur de malloc
pointeur déjà utilisée dans le programme actuel (sauf si vous effectuez quelque chose d’illégal, par exemple l’utilisation de la mémoire après sa libération).
Le restrict
declspec fournit au compilateur plus d’informations pour effectuer des optimisations du compilateur. L’une des choses les plus difficiles pour un compilateur est de déterminer quels pointeurs alias d’autres pointeurs, et l’utilisation de ces informations aide considérablement le compilateur.
Il vaut la peine de souligner qu’il s’agit d’une promesse au compilateur, pas quelque chose que le compilateur vérifiera. Si votre programme utilise ce restrict
déclspec de manière inappropriée, votre programme peut avoir un comportement incorrect.
Pour plus d’informations, consultez restrict
.
Le noalias
declspec est également appliqué uniquement aux fonctions et indique que la fonction est une fonction semi-pure. Une fonction semi-pure est une fonction qui référence ou modifie uniquement les locaux, les arguments et les indirections de premier niveau des arguments. Ce declspec est une promesse pour le compilateur et si la fonction fait référence à des globals ou à des indirections de second niveau des arguments pointeurs, le compilateur peut générer du code qui interrompt l’application.
Pour plus d’informations, consultez noalias
.
Pragmas d’optimisation
Il existe également plusieurs pragmas utiles pour aider à optimiser le code. Le premier dont nous allons discuter est #pragma optimize
:
#pragma optimize("{opt-list}", on | off)
Ce pragma vous permet de définir un niveau d’optimisation donné sur une base de fonction par fonction. Cela est idéal pour ces rares occasions où votre application se bloque lorsqu’une fonction donnée est compilée avec optimisation. Vous pouvez l’utiliser pour désactiver les optimisations pour une fonction unique :
#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)
Pour plus d’informations, consultez optimize
.
Inlining est l’une des optimisations les plus importantes que le compilateur effectue et ici nous parlons de quelques pragmas qui aident à modifier ce comportement.
#pragma inline_recursion
est utile pour spécifier si vous souhaitez que l’application puisse inliner un appel récursif. Par défaut, il est désactivé. Pour une récursivité superficielle des petites fonctions, vous pouvez activer cette fonctionnalité. Pour plus d’informations, consultez inline_recursion
.
Un autre pragma utile pour limiter la profondeur de l’inlining est #pragma inline_depth
. Cela est généralement utile dans les situations où vous essayez de limiter la taille d’un programme ou d’une fonction. Pour plus d’informations, consultez inline_depth
.
__restrict
et __assume
Il existe quelques mots clés dans Visual Studio qui peuvent aider les performances : __restrict et __assume.
Tout d’abord, il convient de noter qu’il __restrict
s’agit de __declspec(restrict)
deux choses différentes. Bien qu’elles soient quelque peu liées, leur sémantique est différente. __restrict
est un qualificateur de type, comme const
ou volatile
, mais exclusivement pour les types de pointeur.
Un pointeur modifié est __restrict
appelé pointeur __restrict. Un pointeur __restrict est un pointeur accessible uniquement via le pointeur __restrict. En d’autres termes, un autre pointeur ne peut pas être utilisé pour accéder aux données pointées par le pointeur __restrict.
__restrict
peut être un outil puissant pour l’optimiseur Microsoft C++, mais l’utiliser avec grand soin. En cas d’utilisation incorrecte, l’optimiseur peut effectuer une optimisation qui interrompt votre application.
Avec __assume
, un développeur peut indiquer au compilateur de faire des hypothèses sur la valeur d’une variable.
Par exemple __assume(a < 5);
, indique à l’optimiseur qu’à cette ligne de code la variable a
est inférieure à 5. Là encore, il s’agit d’une promesse au compilateur. Si a
en fait 6 à ce stade du programme, le comportement du programme après l’optimisation du compilateur peut ne pas être ce que vous attendiez. __assume
est le plus utile avant de changer d’instructions et/ou d’expressions conditionnelles.
Il existe certaines limitations à __assume
. Tout d’abord, comme __restrict
, il n’est qu’une suggestion, donc le compilateur est libre de l’ignorer. En outre, actuellement, __assume
fonctionne uniquement avec des inégalités variables contre des constantes. Elle ne propage pas les inégalités symboliques, par exemple, suppose(a < b).
Prise en charge intrinsèque
Les intrinsèques sont des appels de fonction où le compilateur a des connaissances intrinsèques sur l’appel, et plutôt que d’appeler une fonction dans une bibliothèque, il émet du code pour cette fonction. Le fichier <d’en-tête intrin.h> contient toutes les intrinsèques disponibles pour chacune des plateformes matérielles prises en charge.
Les intrinsèques permettent au programmeur de passer en profondeur dans le code sans avoir à utiliser l’assembly. Il existe plusieurs avantages à l’utilisation des intrinsèques :
Votre code est plus portable. Plusieurs des intrinsèques sont disponibles sur plusieurs architectures de processeur.
Votre code est plus facile à lire, car le code est toujours écrit en C/C++.
Votre code bénéficie des optimisations du compilateur. À mesure que le compilateur s’améliore, la génération de code pour les intrinsèques s’améliore.
Pour plus d’informations, consultez Intrinsèques du compilateur.
Exceptions
Il existe un impact sur les performances associé à l’utilisation d’exceptions. Certaines restrictions sont introduites lors de l’utilisation de blocs try qui empêchent le compilateur d’effectuer certaines optimisations. Sur les plateformes x86, il existe une dégradation des performances supplémentaire des blocs try en raison d’informations d’état supplémentaires qui doivent être générées pendant l’exécution du code. Sur les plateformes 64 bits, les blocs try ne dégradent pas autant les performances, mais une fois qu’une exception est levée, le processus de recherche du gestionnaire et le déroulement de la pile peuvent être coûteux.
Par conséquent, il est recommandé d’éviter d’introduire des blocs try/catch dans du code qui n’en a pas vraiment besoin. Si vous devez utiliser des exceptions, utilisez des exceptions synchrones si possible. Pour plus d'informations, consultez Structured Exception Handling (C/C++).
Enfin, lèvez des exceptions pour des cas exceptionnels uniquement. L’utilisation d’exceptions pour le flux de contrôle général entraînera probablement une baisse des performances.