Il y a de nombreuses années, les compilateurs C n'étaient pas particulièrement intelligents. Comme solution de contournement, K&R a inventé le mot-clé register , pour indiquer au compilateur que ce serait peut-être une bonne idée de conserver cette variable dans un registre interne. Ils ont également fait de l'opérateur tertiaire pour aider à générer un meilleur code.
Au fil du temps, les compilateurs ont mûri. Ils sont devenus très intelligents en ce sens que leur analyse de flux leur a permis de prendre de meilleures décisions sur les valeurs à conserver dans les registres que vous ne pourriez le faire. Le mot-clé register est devenu sans importance.
FORTRAN peut être plus rapide que C pour certains types d'opérations, en raison de problèmes d' alias . En théorie, avec un codage soigneux, on peut contourner cette restriction pour permettre à l'optimiseur de générer un code plus rapide.
Quelles pratiques de codage sont disponibles qui peuvent permettre au compilateur / optimiseur de générer du code plus rapidement?
- Identifier la plate-forme et le compilateur que vous utilisez serait apprécié.
- Pourquoi la technique semble-t-elle fonctionner?
- Un exemple de code est encouragé.
Voici une question connexe
[Modifier] Cette question ne concerne pas le processus global de profilage et d'optimisation. Supposons que le programme a été écrit correctement, compilé avec une optimisation complète, testé et mis en production. Il peut y avoir des constructions dans votre code qui empêchent l'optimiseur de faire le meilleur travail possible. Que pouvez-vous faire pour refactoriser ce qui supprimera ces interdictions et permettra à l'optimiseur de générer un code encore plus rapide?
[Modifier] Lien relatif à la compensation
la source
register
rendu le code sensible aux performances plus portable en combattant les mauvais compilateurs.Réponses:
Écrivez dans des variables locales et ne produisez pas d'arguments! Cela peut être d'une grande aide pour contourner les ralentissements des alias. Par exemple, si votre code ressemble à
le compilateur ne sait pas que foo1! = barOut, et doit donc recharger foo1 à chaque fois dans la boucle. Il ne peut pas non plus lire foo2 [i] tant que l'écriture dans barOut n'est pas terminée. Vous pouvez commencer à jouer avec des pointeurs restreints, mais c'est tout aussi efficace (et beaucoup plus clair) de le faire:
Cela semble idiot, mais le compilateur peut être beaucoup plus intelligent en traitant la variable locale, car il ne peut pas se chevaucher en mémoire avec l'un des arguments. Cela peut vous aider à éviter le redoutable load-hit-store (mentionné par Francis Boivin dans ce fil).
la source
Voici une pratique de codage pour aider le compilateur à créer du code rapide - n'importe quel langage, n'importe quelle plate-forme, n'importe quel compilateur, n'importe quel problème:
N'utilisez pas de trucs astucieux qui forcent, voire encouragent, le compilateur à disposer les variables en mémoire (y compris le cache et les registres) comme vous l'entendez. Commencez par écrire un programme correct et maintenable.
Ensuite, profilez votre code.
Ensuite, et alors seulement, vous voudrez peut-être commencer à étudier les effets de dire au compilateur comment utiliser la mémoire. Apportez 1 changement à la fois et mesurez son impact.
Attendez-vous à être déçu et à devoir travailler très dur pour de petites améliorations de performances. Les compilateurs modernes pour les langages matures tels que Fortran et C sont très, très bons. Si vous lisez un compte rendu d'une «astuce» pour obtenir de meilleures performances du code, gardez à l'esprit que les rédacteurs du compilateur l'ont également lu et, si cela en vaut la peine, l'ont probablement implémenté. Ils ont probablement écrit ce que vous avez lu en premier lieu.
la source
&
contre%
des puissances de deux (rarement, voire jamais, optimisés, mais peuvent avoir des impacts significatifs de la performance). Si vous lisez un truc pour les performances, la seule façon de savoir si cela fonctionne est de faire le changement et de mesurer l'impact. Ne supposez jamais que le compilateur optimisera quelque chose pour vous.n
, gcc remplace% n
par& (n-1)
même lorsque l'optimisation est désactivée . Ce n'est pas exactement "rarement, voire jamais" ...L'ordre dans lequel vous parcourez la mémoire peut avoir un impact profond sur les performances et les compilateurs ne sont pas vraiment doués pour le comprendre et le réparer. Vous devez être conscient des problèmes de localisation du cache lorsque vous écrivez du code si vous vous souciez des performances. Par exemple, les tableaux bidimensionnels en C sont alloués au format ligne principale. Traverser les tableaux dans le format principal de colonne aura tendance à vous faire avoir plus d'erreurs de cache et à rendre votre programme plus lié à la mémoire qu'au processeur:
la source
-floop-interchange
ce qui va inverser une boucle interne et externe si l'optimiseur le juge rentable.Optimisations génériques
Voici quelques-unes de mes optimisations préférées. J'ai en fait augmenté les temps d'exécution et réduit la taille des programmes en les utilisant.
Déclarer de petites fonctions en tant que
inline
macros ouChaque appel à une fonction (ou méthode) entraîne une surcharge, telle que le transfert de variables sur la pile. Certaines fonctions peuvent également entraîner des frais généraux au retour. Une fonction ou une méthode inefficace a moins d'instructions dans son contenu que la surcharge combinée. Ce sont de bons candidats pour l'inlining, que ce soit sous forme de
#define
macros ou deinline
fonctions. (Oui, je sais que ceinline
n'est qu'une suggestion, mais dans ce cas, je le considère comme un rappel au compilateur.)Supprimer le code mort et redondant
Si le code n'est pas utilisé ou ne contribue pas au résultat du programme, supprimez-le.
Simplifiez la conception des algorithmes
Une fois, j'ai supprimé beaucoup de code d'assemblage et de temps d'exécution d'un programme en écrivant l'équation algébrique qu'il calculait, puis j'ai simplifié l'expression algébrique. La mise en œuvre de l'expression algébrique simplifiée a pris moins de place et de temps que la fonction d'origine.
Déroulement de la boucle
Chaque boucle a une surcharge d'incrémentation et de vérification de terminaison. Pour obtenir une estimation du facteur de performance, comptez le nombre d'instructions dans la surcharge (minimum 3: incrémenter, vérifier, aller au début de la boucle) et diviser par le nombre d'instructions à l'intérieur de la boucle. Plus le nombre est bas, mieux c'est.
Edit: fournissez un exemple de déroulement de boucle Avant:
Après déroulement:
Dans cet avantage, un avantage secondaire est obtenu: davantage d'instructions sont exécutées avant que le processeur ne doive recharger le cache d'instructions.
J'ai eu des résultats étonnants lorsque j'ai déroulé une boucle à 32 instructions. C'était l'un des goulots d'étranglement puisque le programme devait calculer une somme de contrôle sur un fichier de 2 Go. Cette optimisation combinée à la lecture de bloc a amélioré les performances de 1 heure à 5 minutes. Le déroulement en boucle offrait également d'excellentes performances en langage d'assemblage, mon
memcpy
était beaucoup plus rapide que celui du compilateurmemcpy
. - TMRéduction des
if
déclarationsLes processeurs détestent les branches, ou les sauts, car cela oblige le processeur à recharger sa file d’instructions.
Arithmétique booléenne ( modifié: format de code appliqué au fragment de code, exemple ajouté)
Convertissez les
if
instructions en affectations booléennes. Certains processeurs peuvent exécuter des instructions de manière conditionnelle sans branchement:Le court-circuit de l' opérateur ET logique (
&&
) empêche l'exécution des tests si lestatus
estfalse
.Exemple:
Allocation de variable de facteur en dehors des boucles
Si une variable est créée à la volée dans une boucle, déplacez la création / allocation avant la boucle. Dans la plupart des cas, la variable n'a pas besoin d'être allouée à chaque itération.
Factoriser les expressions constantes en dehors des boucles
Si un calcul ou une valeur de variable ne dépend pas de l'index de la boucle, déplacez-le en dehors (avant) de la boucle.
E / S par blocs
Lisez et écrivez des données en gros morceaux (blocs). Le plus gros le meilleur. Par exemple, lire un octect à la fois est moins efficace que lire 1024 octets avec une seule lecture.
Exemple:
L'efficacité de cette technique peut être démontrée visuellement. :-)
N'utilisez pas la
printf
famille pour des données constantesLes données constantes peuvent être sorties en utilisant une écriture de bloc. L'écriture formatée perdra du temps à analyser le texte pour le formatage des caractères ou le traitement des commandes de formatage. Voir l'exemple de code ci-dessus.
Formater en mémoire, puis écrire
Formatez en un
char
tableau en utilisant plusieurssprintf
, puis utilisezfwrite
. Cela permet également de diviser la présentation des données en «sections constantes» et sections variables. Pensez au publipostage .Déclarez le texte constant (littéraux de chaîne) comme
static const
Lorsque des variables sont déclarées sans le
static
, certains compilateurs peuvent allouer de l'espace sur la pile et copier les données de la ROM. Ce sont deux opérations inutiles. Cela peut être résolu en utilisant lestatic
préfixe.Enfin, le code comme le compilateur le ferait
Parfois, le compilateur peut optimiser plusieurs petites instructions mieux qu'une version compliquée. En outre, l'écriture de code pour aider le compilateur à optimiser est également utile. Si je veux que le compilateur utilise des instructions spéciales de transfert de bloc, j'écrirai du code qui semble devoir utiliser les instructions spéciales.
la source
fprintf
formats dans un tampon séparé sortent ensuite le tampon. Un rationalisé (pour l'utilisation de la mémoire)fprintf
produirait tout le texte non formaté, puis formaterait et sortirait, et se répéterait jusqu'à ce que la chaîne de format entière soit traitée, faisant ainsi 1 appel de sortie pour chaque type de sortie (formaté ou non formaté). D'autres implémentations auraient besoin d'allouer dynamiquement de la mémoire pour chaque appel pour contenir la nouvelle chaîne entière (ce qui est mauvais dans l'environnement des systèmes embarqués). Ma suggestion réduit le nombre de sorties.L'optimiseur ne contrôle pas vraiment les performances de votre programme, vous l'êtes. Utilisez des algorithmes et des structures appropriés ainsi que le profil, le profil, le profil.
Cela dit, vous ne devriez pas effectuer une boucle interne sur une petite fonction d'un fichier dans un autre fichier, car cela l'empêche d'être insérée.
Évitez de prendre l'adresse d'une variable si possible. Demander un pointeur n'est pas "gratuit" car cela signifie que la variable doit être conservée en mémoire. Même un tableau peut être conservé dans des registres si vous évitez les pointeurs - c'est essentiel pour la vectorisation.
Ce qui mène au point suivant, lisez le manuel ^ # $ @ ! GCC peut vectoriser du code C brut si vous saupoudrez un
__restrict__
ici et un__attribute__( __aligned__ )
là. Si vous voulez quelque chose de très spécifique de l'optimiseur, vous devrez peut-être être spécifique.la source
A.c
se mettre en ligneB.c
.Sur la plupart des processeurs modernes, le plus gros goulot d'étranglement est la mémoire.
Aliasing: Load-Hit-Store peut être dévastateur dans une boucle serrée. Si vous lisez un emplacement mémoire et que vous écrivez dans un autre et que vous savez qu'ils sont disjoints, placer soigneusement un mot-clé alias sur les paramètres de la fonction peut vraiment aider le compilateur à générer du code plus rapidement. Cependant, si les régions de mémoire se chevauchent et que vous avez utilisé «alias», vous êtes prêt pour une bonne session de débogage des comportements non définis!
Cache-miss: Je ne sais pas vraiment comment vous pouvez aider le compilateur car il est principalement algorithmique, mais il y a des éléments intrinsèques pour pré-lire la mémoire.
N'essayez pas non plus de trop convertir les valeurs à virgule flottante en int et vice versa car ils utilisent des registres différents et la conversion d'un type à un autre signifie appeler l'instruction de conversion réelle, écrire la valeur en mémoire et la relire dans le jeu de registres approprié .
la source
La grande majorité du code que les gens écrivent sera lié aux E / S (je crois que tout le code que j'ai écrit pour de l'argent au cours des 30 dernières années a été si lié), donc les activités de l'optimiseur pour la plupart des gens seront académiques.
Cependant, je rappelle aux gens que pour que le code soit optimisé, vous devez dire au compilateur de l'optimiser - beaucoup de gens (y compris moi quand j'oublie) publient ici des benchmarks C ++ qui n'ont pas de sens sans que l'optimiseur soit activé.
la source
utilisez la correction de const autant que possible dans votre code. Cela permet au compilateur d'optimiser beaucoup mieux.
Dans ce document, vous trouverez de nombreux autres conseils d'optimisation: les optimisations CPP (un document un peu ancien cependant)
points forts:
la source
const
etrestrict
n'est cependant pas définie. Un compilateur pourrait donc optimiser différemment dans un tel cas.const
uneconst
référence ou unconst
pointeur vers un non-const
objet est bien défini. modifier unconst
objet réel (c'est-à-dire un objet déclaré comme à l'const
origine) ne l'est pas.Essayez de programmer en utilisant autant que possible une affectation statique unique. SSA est exactement le même que celui avec lequel vous vous retrouvez dans la plupart des langages de programmation fonctionnels, et c'est ce vers quoi la plupart des compilateurs convertissent votre code pour effectuer leurs optimisations, car il est plus facile de travailler avec. En faisant cela, les endroits où le compilateur pourrait être confus sont mis en lumière. Cela permet également à tous les allocateurs de registre, sauf les pires, de fonctionner aussi bien que les meilleurs allocateurs de registre, et vous permet de déboguer plus facilement car vous n'avez presque jamais à vous demander d'où une variable tire sa valeur car il n'y avait qu'un seul endroit où elle a été affectée.
Évitez les variables globales.
Lorsque vous travaillez avec des données par référence ou par pointeur, extrayez-les dans des variables locales, faites votre travail, puis copiez-les. (sauf si vous avez une bonne raison de ne pas le faire)
Utilisez la comparaison presque gratuite avec 0 que la plupart des processeurs vous donnent lorsque vous effectuez des opérations mathématiques ou logiques. Vous obtenez presque toujours un indicateur pour == 0 et <0, à partir duquel vous pouvez facilement obtenir 3 conditions:
est presque toujours moins cher que de tester d'autres constantes.
Une autre astuce consiste à utiliser la soustraction pour éliminer une comparaison dans les tests de portée.
Cela peut très souvent éviter un saut dans les langages qui font des court-circuits sur les expressions booléennes et évite au compilateur d'avoir à essayer de comprendre comment gérer le suivi du résultat de la première comparaison tout en faisant la seconde et en les combinant ensuite. Cela peut sembler avoir le potentiel d'utiliser un registre supplémentaire, mais ce n'est presque jamais le cas. Souvent, vous n'avez plus besoin de foo de toute façon, et si vous ne le faites pas, rc n'est pas encore utilisé, il peut y aller.
Lorsque vous utilisez les fonctions de chaîne dans c (strcpy, memcpy, ...), souvenez-vous de ce qu'elles renvoient - la destination! Vous pouvez souvent obtenir un meilleur code en «oubliant» votre copie du pointeur vers la destination et en la récupérant simplement au retour de ces fonctions.
Ne négligez jamais l'opportunité de renvoyer exactement la même chose que la dernière fonction que vous avez appelée a renvoyée. Les compilateurs ne sont pas si doués pour comprendre que:
Bien sûr, vous pouvez inverser la logique à ce sujet si et seulement avoir un point de retour.
(trucs dont je me suis souvenu plus tard)
Déclarer des fonctions statiques lorsque vous le pouvez est toujours une bonne idée. Si le compilateur peut se prouver qu'il a pris en compte chaque appelant d'une fonction particulière, il peut alors rompre les conventions d'appel pour cette fonction au nom de l'optimisation. Les compilateurs peuvent souvent éviter de déplacer des paramètres dans des registres ou des positions de pile dans lesquelles les fonctions appelées s'attendent généralement à ce que leurs paramètres soient (il doit dévier à la fois de la fonction appelée et de l'emplacement de tous les appelants pour ce faire). Le compilateur peut aussi souvent profiter de la connaissance de la mémoire et des registres dont la fonction appelée aura besoin et éviter de générer du code pour conserver les valeurs de variables qui se trouvent dans des registres ou des emplacements mémoire que la fonction appelée ne perturbe pas. Cela fonctionne particulièrement bien lorsqu'il y a peu d'appels à une fonction.
la source
J'ai écrit un compilateur d'optimisation C et voici quelques éléments très utiles à considérer:
Rendre la plupart des fonctions statiques. Cela permet à la propagation interprocédurale constante et à l'analyse d'alias de faire leur travail, sinon le compilateur doit présumer que la fonction peut être appelée depuis l'extérieur de l'unité de traduction avec des valeurs complètement inconnues pour les paramètres. Si vous regardez les bibliothèques open-source bien connues, elles marquent toutes les fonctions statiques à l'exception de celles qui doivent vraiment être externes.
Si des variables globales sont utilisées, marquez-les si possible comme statiques et constantes. S'ils sont initialisés une fois (lecture seule), il est préférable d'utiliser une liste d'initialisation comme static const int VAL [] = {1,2,3,4}, sinon le compilateur pourrait ne pas découvrir que les variables sont en fait des constantes initialisées et ne remplacera pas les charges de la variable par les constantes.
N'utilisez JAMAIS un goto à l'intérieur d'une boucle, la boucle ne sera plus reconnue par la plupart des compilateurs et aucune des optimisations les plus importantes ne sera appliquée.
Utilisez des paramètres de pointeur uniquement si nécessaire et marquez-les comme restrictifs si possible. Cela aide beaucoup l'analyse des alias car le programmeur garantit qu'il n'y a pas d'alias (l'analyse des alias interprocédurale est généralement très primitive). Les très petits objets struct doivent être passés par valeur, pas par référence.
Utilisez des tableaux au lieu de pointeurs chaque fois que possible, en particulier à l'intérieur des boucles (a [i]). Un tableau offre généralement plus d'informations pour l'analyse d'alias et après quelques optimisations, le même code sera généré de toute façon (recherchez la réduction de la force de la boucle si curieux). Cela augmente également les chances d'appliquer un mouvement de code invariant en boucle.
Essayez de hisser en dehors de la boucle les appels à de grandes fonctions ou à des fonctions externes qui n'ont pas d'effets secondaires (ne dépendez pas de l'itération de boucle actuelle). Les petites fonctions sont dans de nombreux cas incorporées ou converties en éléments intrinsèques qui sont faciles à hisser, mais les fonctions volumineuses peuvent sembler au compilateur avoir des effets secondaires alors qu'elles n'en ont pas. Les effets secondaires pour les fonctions externes sont complètement inconnus, à l'exception de certaines fonctions de la bibliothèque standard qui sont parfois modélisées par certains compilateurs, rendant possible le mouvement de code invariant en boucle.
Lorsque vous écrivez des tests avec plusieurs conditions, placez le plus probable en premier. if (a || b || c) doit être if (b || a || c) si b est plus vraisemblablement vrai que les autres. Les compilateurs ne savent généralement rien des valeurs possibles des conditions et des branches qui sont le plus utilisées (elles pourraient être connues en utilisant les informations de profil, mais peu de programmeurs l'utilisent).
Utiliser un commutateur est plus rapide que de faire un test comme if (a || b || ... || z). Vérifiez d'abord si votre compilateur fait cela automatiquement, certains le font et il est plus lisible d'avoir le if .
la source
Dans le cas des systèmes embarqués et du code écrit en C / C ++, j'essaie d'éviter autant que possible l' allocation de mémoire dynamique . La principale raison pour laquelle je fais cela n'est pas nécessairement la performance, mais cette règle empirique a des implications sur les performances.
Les algorithmes utilisés pour gérer le tas sont notoirement lents sur certaines plates-formes (par exemple, vxworks). Pire encore, le temps nécessaire pour revenir d'un appel à malloc dépend fortement de l'état actuel du tas. Par conséquent, toute fonction qui appelle malloc va subir une baisse de performances qui ne peut pas être facilement prise en compte. Cet impact sur les performances peut être minime si le tas est toujours propre, mais après l'exécution de ce périphérique pendant un certain temps, le tas peut devenir fragmenté. Les appels vont prendre plus de temps et vous ne pouvez pas calculer facilement comment les performances se dégraderont au fil du temps. Vous ne pouvez pas vraiment produire une estimation de cas pire. L'optimiseur ne peut pas non plus vous fournir d'aide dans ce cas. Pour aggraver les choses, si le tas devient trop fragmenté, les appels commenceront à échouer complètement. La solution consiste à utiliser des pools de mémoire (par exemple,glib slices ) au lieu du tas. Les appels d'allocation seront beaucoup plus rapides et déterministes si vous le faites correctement.
la source
Un petit conseil stupide, mais qui vous fera économiser des quantités microscopiques de vitesse et de code.
Passez toujours les arguments de fonction dans le même ordre.
Si vous avez f_1 (x, y, z) qui appelle f_2, déclarez f_2 comme f_2 (x, y, z). Ne le déclarez pas comme f_2 (x, z, y).
La raison en est que la plate-forme C / C ++ ABI (convention d'appel AKA) promet de passer des arguments dans des registres et des emplacements de pile particuliers. Lorsque les arguments sont déjà dans les bons registres, il n'est pas nécessaire de les déplacer.
En lisant du code désassemblé, j'ai vu un mélange ridicule de registre parce que les gens ne suivaient pas cette règle.
la source
Deux techniques de codage que je n'ai pas vues dans la liste ci-dessus:
Contourner l'éditeur de liens en écrivant du code comme source unique
Bien que la compilation séparée soit vraiment agréable pour la compilation, elle est très mauvaise quand vous parlez d'optimisation. Fondamentalement, le compilateur ne peut pas optimiser au-delà de l'unité de compilation, qui est le domaine réservé à l'éditeur de liens.
Mais si vous concevez bien votre programme, vous pouvez également le compiler via une source commune unique. C'est-à-dire au lieu de compiler unit1.c et unit2.c, liez les deux objets, compilez all.c qui contient simplement #include unit1.c et unit2.c. Ainsi, vous bénéficierez de toutes les optimisations du compilateur.
C'est très comme écrire des en-têtes uniquement des programmes en C ++ (et encore plus facile à faire en C).
Cette technique est assez simple si vous écrivez votre programme pour l'activer depuis le début, mais vous devez également être conscient que cela change une partie de la sémantique en C et que vous pouvez rencontrer des problèmes comme des variables statiques ou une collision de macros. Pour la plupart des programmes, il est assez facile de surmonter les petits problèmes qui surviennent. Sachez également que la compilation en tant que source unique est beaucoup plus lente et peut prendre une énorme quantité de mémoire (généralement pas un problème avec les systèmes modernes).
En utilisant cette technique simple, il m'est arrivé de créer des programmes que j'ai écrits dix fois plus vite!
Comme le mot-clé register, cette astuce pourrait également devenir bientôt obsolète. L'optimisation via l'éditeur de liens commence à être prise en charge par les compilateurs gcc: Optimisation du temps de liaison .
Séparez les tâches atomiques en boucles
Celui-ci est plus délicat. Il s'agit de l'interaction entre la conception de l'algorithme et la façon dont l'optimiseur gère le cache et l'allocation des registres. Très souvent, les programmes doivent boucler sur une structure de données et pour chaque élément effectuer certaines actions. Très souvent, les actions effectuées peuvent être réparties entre deux tâches logiquement indépendantes. Si tel est le cas, vous pouvez écrire exactement le même programme avec deux boucles sur la même limite en effectuant exactement une tâche. Dans certains cas, l'écrire de cette façon peut être plus rapide que la boucle unique (les détails sont plus complexes, mais une explication peut être qu'avec le cas de tâche simple, toutes les variables peuvent être conservées dans les registres du processeur et avec le plus complexe, ce n'est pas possible et certaines les registres doivent être écrits dans la mémoire et relus ultérieurement et le coût est plus élevé que le contrôle de flux supplémentaire).
Soyez prudent avec celui-ci (performances du profil en utilisant cette astuce ou non) car comme en utilisant le registre, il peut aussi bien donner des performances moindres que celles améliorées.
la source
J'ai en fait vu cela faire dans SQLite et ils prétendent que cela entraîne une amélioration des performances d'environ 5%: mettez tout votre code dans un fichier ou utilisez le préprocesseur pour faire l'équivalent de cela. De cette façon, l'optimiseur aura accès à l'ensemble du programme et pourra effectuer davantage d'optimisations interprocédurales.
la source
-O3
- il a éliminé 22% de la taille d'origine de mon programme. (Ce n'est pas lié au processeur, donc je n'ai pas grand chose à dire sur la vitesse.)La plupart des compilateurs modernes devraient faire un bon travail en accélérant la récursivité de la queue , car les appels de fonction peuvent être optimisés.
Exemple:
Bien sûr, cet exemple n'a pas de vérification des limites.
Modification tardive
Bien que je n'ai aucune connaissance directe du code; il semble clair que les exigences relatives à l'utilisation des CTE sur SQL Server ont été spécifiquement conçues pour pouvoir être optimisées via la récursivité finale.
la source
Ne faites pas le même travail encore et encore!
Un anti-modèle commun que je vois va dans ce sens:
Le compilateur doit en fait appeler toutes ces fonctions tout le temps. En supposant que vous, le programmeur, sachez que l'objet agrégé ne change pas au cours de ces appels, pour l'amour de tout ce qui est saint ...
Dans le cas du getter singleton, les appels ne sont peut-être pas trop coûteux, mais c'est certainement un coût (généralement, "vérifier si l'objet a été créé, si ce n'est pas le cas, créez-le, puis renvoyez-le). plus cette chaîne de getters se complique, plus nous perdrons de temps.
la source
Utilisez la portée la plus locale possible pour toutes les déclarations de variables.
Utilisez
const
autant que possibleN'utilisez pas le registre sauf si vous prévoyez de profiler à la fois avec et sans lui
Les 2 premiers d'entre eux, en particulier le numéro 1, aident l'optimiseur à analyser le code. Cela l'aidera particulièrement à faire de bons choix sur les variables à conserver dans les registres.
L'utilisation aveugle du mot-clé register est aussi susceptible d'aider que de nuire à votre optimisation.Il est tout simplement trop difficile de savoir ce qui importera jusqu'à ce que vous regardiez la sortie ou le profil de l'assemblage.
Il y a d'autres choses qui comptent pour obtenir de bonnes performances du code; concevoir vos structures de données pour maximiser la cohérence du cache par exemple. Mais la question concernait l'optimiseur.
la source
Alignez vos données sur les limites natives / naturelles.
la source
Cela m'a rappelé quelque chose que j'ai rencontré une fois, où le symptôme était simplement que nous manquions de mémoire, mais le résultat était une augmentation substantielle des performances (ainsi que d'énormes réductions de l'empreinte mémoire).
Le problème dans ce cas était que le logiciel que nous utilisions faisait des tonnes de petites allocations. Comme, allouer quatre octets ici, six octets là-bas, etc. Beaucoup de petits objets aussi, fonctionnant dans la plage de 8 à 12 octets. Le problème n'était pas tant que le programme avait besoin de beaucoup de petites choses, c'est qu'il allouait beaucoup de petites choses individuellement, ce qui gonflait chaque allocation à (sur cette plate-forme particulière) 32 octets.
Une partie de la solution consistait à créer un petit pool d'objets de style Alexandrescu, mais à l'étendre pour que je puisse allouer des tableaux de petits objets ainsi que des éléments individuels. Cela a également énormément contribué aux performances, car plus d'éléments rentrent dans le cache à tout moment.
L'autre partie de la solution consistait à remplacer l'utilisation rampante des membres char * gérés manuellement par une chaîne SSO (optimisation de petite chaîne). L'allocation minimale étant de 32 octets, j'ai construit une classe de chaînes qui avait un tampon de 28 caractères intégré derrière un caractère *, donc 95% de nos chaînes n'avaient pas besoin de faire une allocation supplémentaire (puis j'ai remplacé manuellement presque chaque apparence de char * dans cette bibliothèque avec cette nouvelle classe, c'était amusant ou pas). Cela a également beaucoup contribué à la fragmentation de la mémoire, ce qui a ensuite augmenté la localité de référence pour d'autres objets pointés, et de même, des gains de performances ont été réalisés.
la source
Une technique soignée que j'ai apprise du commentaire de @MSalters sur cette réponse permet aux compilateurs de faire une élision de copie même lorsqu'ils retournent différents objets selon certaines conditions:
la source
Si vous avez de petites fonctions que vous appelez à plusieurs reprises, j'ai par le passé obtenu de gros gains en les mettant dans des en-têtes comme "static inline". Les appels de fonction sur l'ix86 sont étonnamment chers.
Réimplémenter des fonctions récursives de manière non récursive à l'aide d'une pile explicite peut également gagner beaucoup, mais alors vous êtes vraiment dans le domaine du temps de développement par rapport au gain.
la source
Voici mon deuxième conseil d'optimisation. Comme pour mon premier conseil, il s'agit d'un objectif général, et non d'un langage ou d'un processeur spécifique.
Lisez attentivement le manuel du compilateur et comprenez ce qu'il vous dit. Utilisez le compilateur au maximum.
Je suis d'accord avec un ou deux des autres répondants qui ont identifié le choix du bon algorithme comme essentiel pour réduire les performances d'un programme. Au-delà de cela, le taux de retour (mesuré dans l'amélioration de l'exécution du code) sur le temps que vous investissez dans l'utilisation du compilateur est bien supérieur au taux de retour sur le peaufinage du code.
Oui, les rédacteurs de compilateurs ne sont pas issus d'une race de géants du codage et les compilateurs contiennent des erreurs et ce qui devrait, selon le manuel et selon la théorie du compilateur, rendre les choses plus rapides ralentit parfois les choses. C'est pourquoi vous devez faire un pas à la fois et mesurer les performances avant et après le réglage.
Et oui, en fin de compte, vous pourriez être confronté à une explosion combinatoire d'indicateurs du compilateur, vous devez donc avoir un script ou deux pour exécuter make avec divers indicateurs du compilateur, mettre en file d'attente les travaux sur le grand cluster et collecter les statistiques d'exécution. S'il ne s'agit que de vous et de Visual Studio sur un PC, vous manquerez d'intérêt bien avant d'avoir essayé suffisamment de combinaisons d'indicateurs de compilateur.
Cordialement
marque
Lorsque je prends un morceau de code pour la première fois, je peux généralement obtenir un facteur de 1,4 à 2,0 fois plus de performances (c'est-à-dire que la nouvelle version du code fonctionne en 1 / 1,4 ou 1/2 du temps de l'ancienne version) en un jour ou deux en jouant avec les indicateurs du compilateur. Certes, cela peut être un commentaire sur le manque de connaissances en compilateur parmi les scientifiques qui sont à l'origine d'une grande partie du code sur lequel je travaille, plutôt qu'un symptôme de mon excellence. Après avoir défini les indicateurs du compilateur sur max (et c'est rarement juste -O3), cela peut prendre des mois de travail acharné pour obtenir un autre facteur de 1,05 ou 1,1
la source
Lorsque DEC est sorti avec ses processeurs alpha, il y avait une recommandation de garder le nombre d'arguments d'une fonction sous 7, car le compilateur essayait toujours de mettre automatiquement jusqu'à 6 arguments dans les registres.
la source
Pour les performances, concentrez-vous d'abord sur l'écriture de code maintenable - en composants, faiblement couplé, etc., donc lorsque vous devez isoler une pièce pour réécrire, optimiser ou simplement profiler, vous pouvez le faire sans trop d'effort.
L'optimiseur améliorera légèrement les performances de votre programme.
la source
Vous obtenez de bonnes réponses ici, mais ils supposent que votre programme est assez proche de l'optimum au départ, et vous dites
D'après mon expérience, un programme peut être écrit correctement, mais cela ne veut pas dire qu'il est presque optimal. Il faut un travail supplémentaire pour en arriver là.
Si je peux donner un exemple, cette réponse montre comment un programme d'apparence parfaitement raisonnable a été réalisé plus de 40 fois plus rapidement par macro-optimisation . De grandes accélérations ne peuvent pas être effectuées dans tous programmes tels qu'ils ont été écrits pour la première fois, mais dans beaucoup (sauf pour les très petits programmes), c'est possible, d'après mon expérience.
Une fois cela fait, la micro-optimisation (des points chauds) peut vous donner un bon gain.
la source
j'utilise le compilateur Intel. sur Windows et Linux.
quand plus ou moins fait, je profile le code. puis accrochez-vous aux hotspots et essayez de changer le code pour permettre au compilateur de faire un meilleur travail.
si un code est un code de calcul et contient beaucoup de boucles - le rapport de vectorisation dans le compilateur Intel est très utile - recherchez 'vec-report' dans l'aide.
donc l'idée principale - polir le code critique de performance. pour le reste - la priorité est d'être correct et maintenable - des fonctions courtes, un code clair compréhensible 1 an plus tard.
la source
Une optimisation que j'ai utilisée en C ++ est la création d'un constructeur qui ne fait rien. Il faut appeler manuellement un init () pour mettre l'objet dans un état de fonctionnement.
Cela a un avantage dans le cas où j'ai besoin d'un grand vecteur de ces classes.
J'appelle reserve () pour allouer l'espace pour le vecteur, mais le constructeur ne touche pas réellement la page de mémoire sur laquelle se trouve l'objet. J'ai donc dépensé un peu d'espace d'adressage, mais pas vraiment consommé beaucoup de mémoire physique. J'évite les défauts de page associés aux coûts de construction associés.
Lorsque je génère des objets pour remplir le vecteur, je les définis en utilisant init (). Cela limite mon nombre total de défauts de page et évite d'avoir à redimensionner () le vecteur tout en le remplissant.
la source
Une chose que j'ai faite est d'essayer de conserver les actions coûteuses aux endroits où l'utilisateur pourrait s'attendre à ce que le programme retarde un peu. La performance globale est liée à la réactivité, mais n'est pas tout à fait la même, et pour beaucoup de choses, la réactivité est la partie la plus importante de la performance.
La dernière fois que j'ai vraiment dû améliorer les performances globales, j'ai gardé un œil sur les algorithmes sous-optimaux et j'ai recherché des endroits susceptibles d'avoir des problèmes de cache. J'ai d'abord dressé le profil et mesuré les performances, puis après chaque changement. Ensuite, l'entreprise s'est effondrée, mais c'était quand même un travail intéressant et instructif.
la source
Je soupçonne depuis longtemps, mais je n'ai jamais prouvé que déclarer des tableaux de sorte qu'ils détiennent une puissance de 2, comme le nombre d'éléments, permet à l'optimiseur de réduire la force en remplaçant une multiplication par un décalage par un certain nombre de bits, lors de la recherche éléments individuels.
la source
val * 7
transformé en ce qui ressemblerait autrement(val << 3) - val
.Placez les petites fonctions et / ou fréquemment appelées en haut du fichier source. Cela permet au compilateur de trouver plus facilement des opportunités d'insertion.
la source