Pratiques de codage qui permettent au compilateur / optimiseur de créer un programme plus rapide

Il y a de nombreuses années, les compilateurs C n’étaient pas particulièrement intelligents. En guise de solution de contournement, K & R a inventé le mot-clé register , afin de suggérer au compilateur, que ce serait peut-être une bonne idée de conserver cette variable dans un registre interne. Ils ont également fait 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 dans leur parsing des stream, ce qui leur a permis de prendre de meilleures décisions sur les valeurs à conserver dans les registres. Le mot-clé de registre est devenu sans importance.

FORTRAN peut être plus rapide que C pour certaines opérations, en raison de problèmes d’ alias . En théorie, avec un codage soigné, on peut contourner cette ressortingction pour permettre à l’optimiseur de générer plus rapidement du code.

Quelles pratiques de codage sont disponibles pour 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?
  • Exemple de code est encouragé.

Voici une question connexe

[Modifier] Cette question ne concerne pas l’ensemble du processus à profiler et à optimiser. Supposons que le programme a été écrit correctement, compilé avec une optimisation complète, testé et mis en production. Votre code peut contenir des constructions qui empêchent l’optimiseur de faire le meilleur travail possible. Que pouvez-vous faire pour refactoriser ce qui supprimera ces interdictions et permettre à l’optimiseur de générer un code encore plus rapide?

[Modifier] Lien lié au décalage

Écrivez dans les variables locales et non dans les arguments! Cela peut être une aide considérable pour contourner les ralentissements d’alias. Par exemple, si votre code ressemble à

 void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut) { for (int i=0; i 

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 sur barOut n’est pas terminée. Vous pourriez commencer à déconner avec des pointeurs restreints, mais il est tout aussi efficace (et beaucoup plus clair) de le faire:

 void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut) { Foo barTemp = barOut; for (int i=0; i 

Cela semble idiot, mais le compilateur peut être beaucoup plus intelligent avec la variable locale, car il ne peut pas se chevaucher en mémoire avec aucun des arguments. Cela peut vous aider à éviter le redoutable load-hit-store (mentionné par Francis Boivin dans ce fil).

Voici une méthode de codage pour aider le compilateur à créer du code rapide: tout langage, toute plate-forme, tout compilateur, tout problème:

N’utilisez pas de trucs astucieux qui forcent, ou même encouragent, le compilateur à mettre des variables en mémoire (y compris le cache et les registres) comme vous le pensez. Écrivez d’abord un programme qui est correct et maintenable.

Ensuite, profilez votre code.

Alors, et alors seulement, vous voudrez peut-être commencer à étudier les effets de dire au compilateur comment utiliser la mémoire. Effectuez 1 changement à la fois et mesurez son impact.

Attendez-vous à être déçu et à travailler très dur pour de petites améliorations de performance. 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 en code, gardez à l’esprit que les auteurs du compilateur ont également lu et, si cela vaut la peine, l’ont probablement implémenté. Ils ont probablement écrit ce que vous avez lu en premier lieu.

L’ordre dans lequel vous traversez la mémoire peut avoir de profondes répercussions sur les performances et les compilateurs ne sont pas vraiment à même de résoudre ce problème. Vous devez être conscient des problèmes de localité de cache lorsque vous écrivez du code si vous vous souciez des performances. Par exemple, les tableaux à deux dimensions en C sont alloués en format ligne-majeure. Le fait de traverser des tableaux dans le format majeur de la colonne a tendance à vous faire perdre plus de cache et à rendre votre programme plus lié à la mémoire que celui lié au processeur:

 #define N 1000000; int masortingx[N][N] = { ... }; //awesomely fast long sum = 0; for(int i = 0; i < N; i++){ for(int j = 0; j < N; j++){ sum += matrix[i][j]; } } //painfully slow long sum = 0; for(int i = 0; i < N; i++){ for(int j = 0; j < N; j++){ sum += matrix[j][i]; } } 

Optimisations Génériques

Voici quelques unes de mes optimisations préférées. J’ai effectivement augmenté les temps d’exécution et réduit les tailles de programme en les utilisant.

Déclarez les petites fonctions comme inline ou macros

Chaque appel à une fonction (ou à une méthode) entraîne des frais généraux, tels que l’envoi de variables sur la stack. Certaines fonctions peuvent également entraîner une surcharge lors du retour. Une fonction ou une méthode inefficace contient moins d’instructions que la surcharge combinée. Ce sont de bons candidats pour l’inlining, que ce soit en tant que macros #define ou fonctions en inline . (Oui, je sais que l’ inline 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 consortingbue pas au résultat du programme, éliminez-le.

Simplifier 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 prend moins de place et de temps que la fonction d’origine.

Déroulement de la boucle

Chaque boucle a un surcoût d’incrémentation et de vérification de terminaison. Pour obtenir une estimation du facteur de performance, comptez le nombre d’instructions dans le temps système (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: fournit un exemple de déroulement en boucle Avant:

 unsigned int sum = 0; for (size_t i; i < BYTES_TO_CHECKSUM; ++i) { sum += *buffer++; } 

Après avoir déroulé:

 unsigned int sum = 0; size_t i = 0; **const size_t STATEMENTS_PER_LOOP = 8;** for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**) { sum += *buffer++; // 1 sum += *buffer++; // 2 sum += *buffer++; // 3 sum += *buffer++; // 4 sum += *buffer++; // 5 sum += *buffer++; // 6 sum += *buffer++; // 7 sum += *buffer++; // 8 } // Handle the remainder: for (; i < BYTES_TO_CHECKSUM; ++i) { sum += *buffer++; } 

Dans cet avantage, un avantage secondaire est obtenu: davantage d'instructions sont exécutées avant que le processeur n'ait à recharger le cache d'instructions.

J'ai eu des résultats étonnants lorsque j'ai déroulé une boucle à 32 déclarations. C'était l'un des goulots d'étranglement puisque le programme devait calculer une sum de contrôle sur un fichier de 2 Go. Cette optimisation combinée à la lecture des blocs a amélioré les performances de 1 heure à 5 minutes. Le déroulement des boucles a également fourni d'excellentes performances en langage d'assemblage, mon memcpy était beaucoup plus rapide que le memcpy du compilateur. - TM

Réduction des déclarations if

Les processeurs détestent les twigs ou les sauts, car cela oblige le processeur à recharger sa file d’instructions.

Arithmétique booléenne ( Édité: format de code appliqué à un fragment de code, exemple ajouté)

Convertir les instructions if en affectations booléennes. Certains processeurs peuvent exécuter des instructions sans condition:

 bool status = true; status = status && /* first test */; status = status && /* second test */; 

Le court-circuit de l'opérateur logique AND ( && ) empêche l'exécution des tests si le status est false .

Exemple:

 struct Reader_Interface { virtual bool write(unsigned int value) = 0; }; struct Rectangle { unsigned int origin_x; unsigned int origin_y; unsigned int height; unsigned int width; bool write(Reader_Interface * p_reader) { bool status = false; if (p_reader) { status = p_reader->write(origin_x); status = status && p_reader->write(origin_y); status = status && p_reader->write(height); status = status && p_reader->write(width); } return status; }; 

Allocation à facteur variable en dehors des boucles

Si une variable est créée à la volée dans une boucle, déplacez la création / allocation vers la boucle. Dans la plupart des cas, la variable n'a pas besoin d'être allouée lors de chaque itération.

Expressions constantes de facteur en dehors des boucles

Si la valeur d'un calcul ou d'une variable ne dépend pas de l'index de la boucle, déplacez-le en dehors de la boucle.

I / O en blocs

Lire et écrire des données en gros morceaux (blocs). Le plus gros le meilleur. Par exemple, lire un octect à la fois est moins efficace que lire 1 024 octets avec une lecture.
Exemple:

 static const char Menu_Text[] = "\n" "1) Print\n" "2) Insert new customer\n" "3) Destroy\n" "4) Launch Nasal Demons\n" "Enter selection: "; static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0'); //... std::cout.write(Menu_Text, Menu_Text_Length); 

L'efficacité de cette technique peut être démontrée visuellement. 🙂

N'utilisez pas la famille printf pour des données constantes

Les données constantes peuvent être sorties en utilisant une écriture de bloc. L'écriture formatée perd du temps à numériser le texte pour formater les caractères ou traiter les commandes de formatage. Voir l'exemple de code ci-dessus.

Formater en mémoire, puis écrire

Formatez-vous dans un tableau de caractères en utilisant plusieurs sprintf , puis utilisez fwrite . Cela permet également de diviser la disposition des données en "sections constantes" et sections variables. Pensez au mail-fusion .

Déclarez le texte constant (littéraux de chaîne) en tant que static const

Lorsque des variables sont déclarées sans le static , certains compilateurs peuvent allouer de l'espace sur la stack et copier les données de la ROM. Ce sont deux opérations inutiles. Cela peut être corrigé en utilisant le préfixe static .

Enfin, le code comme le compilateur serait

Parfois, le compilateur peut optimiser plusieurs petites instructions mieux qu'une version compliquée. Aussi, écrire du code pour aider le compilateur à optimiser aide aussi. Si je veux que le compilateur utilise des instructions spéciales de transfert de bloc, j'écrirai du code qui devrait utiliser les instructions spéciales.

L’optimiseur ne contrôle pas vraiment les performances de votre programme, vous êtes. Utilisez les algorithmes et structures et profils appropriés, le profil et le profil.

Cela dit, vous ne devez pas effectuer de boucle interne sur une petite fonction à partir d’un fichier dans un autre fichier, car cela l’empêche d’être intégré.

É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 – cela est essentiel pour la vectorisation.

Ce qui conduit au point suivant, lisez le manuel ^ # $ @ ! GCC peut vectoriser du code C ordinaire si vous saupoudrez un __ressortingct__ ici et un __atsortingbute__( __aligned__ ) . Si vous voulez quelque chose de très spécifique de l’optimiseur, vous devrez peut-être être spécifique.

Sur la plupart des processeurs modernes, le plus gros problème est la mémoire.

Aliasing: Load-Hit-Store peut être dévastateur dans une boucle serrée. Si vous lisez un emplacement de mémoire et que vous écrivez dans un autre et que vous savez qu’ils sont disjoints, placer un mot-clé alias sur les parameters de la fonction peut aider le compilateur à générer du code plus rapidement. Toutefois, si les régions de mémoire se chevauchent et que vous utilisez «alias», vous êtes prêt pour une bonne session de débogage de comportements indéfinis!

Cache-miss: Pas vraiment sûr de savoir comment vous pouvez aider le compilateur car il est principalement algorithmique, mais il existe des éléments insortingnsèques pour pré-récupérer la mémoire.

N’essayez pas non plus de convertir les valeurs à virgule flottante en int et inversement car elles 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 lire dans le jeu de registres approprié .

La grande majorité du code que les gens écrivent sera liée 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é tellement limité), les activités de l’optimiseur seront donc académiques.

Cependant, je voudrais rappeler aux gens que pour que le code soit optimisé, il faut que le compilateur l’optimise – beaucoup de personnes (y compris moi quand j’oublie) publient ici des tests de performance sans intérêt sans l’optimiseur.

utilisez autant que possible le correct correct dans votre code. Cela permet au compilateur de s’optimiser beaucoup mieux.

Dans ce document, vous trouverez des tas d’autres astuces d’optimisation: optimisations CPP (un document un peu ancien)

points forts:

  • utiliser les listes d’initialisation du constructeur
  • utiliser des opérateurs de préfixe
  • utiliser des constructeurs explicites
  • fonctions en ligne
  • éviter les objects temporaires
  • être conscient du coût des fonctions virtuelles
  • renvoyer des objects via des parameters de référence
  • considérer par allocation de classe
  • Considérer les allocateurs de conteneurs stl
  • l’optimisation ‘membre vide’
  • etc

Tenter de programmer en utilisant une assignation statique unique autant que possible. SSA est exactement identique à ce que vous obtenez dans la plupart des langages de programmation fonctionnels, et c’est ce que la plupart des compilateurs convertissent en optimisation car il est plus facile de travailler avec. En faisant cela, les endroits où le compilateur peut être confus sont mis en évidence. Cela fait aussi que tous les allocateurs, à l’exception des plus mauvais, fonctionnent aussi bien que les meilleurs allocateurs de registres, et vous permettent de déboguer plus facilement parce que vous n’avez presque jamais à vous demander d’où vient la valeur d’une variable.
Évitez les variables globales.

Lorsque vous utilisez des données par référence ou un pointeur, intégrez-les dans des variables locales, faites votre travail, puis recopiez-les. (sauf si vous avez une bonne raison de ne pas le faire)

Utilisez la comparaison presque gratuite contre 0 que la plupart des processeurs vous donnent lorsque vous effectuez des opérations mathématiques ou logiques. Vous obtenez presque toujours un drapeau pour == 0 et <0, à partir duquel vous pouvez facilement obtenir 3 conditions:

 x= f(); if(!x){ a(); } else if (x<0){ b(); } else { c(); } 

est presque toujours moins cher que de tester d'autres constantes.

Une autre astuce consiste à utiliser la soustraction pour éliminer un test de comparaison dans l'intervalle.

 #define FOO_MIN 8 #define FOO_MAX 199 int good_foo(int foo) { unsigned int bar = foo-FOO_MIN; int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0; return rc; } 

Cela peut très souvent éviter un saut dans les langages qui font des courts-circuits sur les expressions booléennes et évite au compilateur d'essayer de comprendre comment gérer le résultat de la première comparaison tout en les combinant. Cela peut sembler avoir le potentiel d'utiliser un registre supplémentaire, mais cela ne se produit presque jamais. Souvent, vous n'avez plus besoin de foo, et si vous ne le faites pas, rc n'est pas encore utilisé pour pouvoir y aller.

Lorsque vous utilisez les fonctions de chaîne dans c (strcpy, memcpy, ...), rappelez-vous ce qu'elles renvoient - la destination! Vous pouvez souvent obtenir un meilleur code en «oubliant» votre copie du pointeur vers la destination et récupérez-le simplement du retour de ces fonctions.

Ne négligez jamais l'opportunité de retourner exactement la même chose que la dernière fonction que vous avez appelée. Les compilateurs ne sont pas si doués pour comprendre que:

 foo_t * make_foo(int a, int b, int c) { foo_t * x = malloc(sizeof(foo)); if (!x) { // return NULL; return x; // x is NULL, already in the register used for returns, so duh } x->a= a; x->b = b; x->c = c; return x; } 

Bien sûr, vous pouvez inverser la logique à ce sujet si et seulement un seul sharepoint retour.

(astuces que je rappelais plus tard)

Déclarer des fonctions statiques quand 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 parameters dans des registres ou des positions de stack que les fonctions appelées attendent généralement de leurs parameters (il doit s'écarter à la fois de la fonction appelée et de l'emplacement de tous les appelants). Le compilateur peut aussi souvent tirer parti de la connaissance de la mémoire et des registres nécessaires à la fonction appelée et éviter de générer du code pour préserver les valeurs de variables figurant dans les registres ou les emplacements de mémoire que la fonction appelée ne perturbe pas. Cela fonctionne particulièrement bien lorsqu'il y a peu d'appels à une fonction. Cela profite en grande partie de l’inclusion du code, mais sans l’inclure.

J’ai écrit un compilateur d’optimisation C et voici quelques points très utiles à prendre en compte:

  1. Rendez la plupart des fonctions statiques. Cela permet à la propagation interprocédure constante et à l’parsing d’alias de faire son travail, sinon le compilateur doit supposer que la fonction peut être appelée depuis l’extérieur de l’unité de traduction avec des valeurs complètement inconnues pour les parameters. Si vous regardez les bibliothèques open-source bien connues, elles marquent toutes des fonctions statiques, à l’exception de celles qui doivent vraiment être externalisées.

  2. Si des variables globales sont utilisées, marquez-les statiques et constantes si possible. S’ils sont initialisés une seule 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 risque de ne pas découvrir que les variables sont en réalité des constantes initialisées et ne parviendra pas à remplacer les charges de la variable avec les constantes.

  3. 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.

  4. Utilisez les parameters du pointeur uniquement si nécessaire et marquez-les si possible. Cela aide beaucoup l’parsing des alias car le programmeur garantit qu’il n’y a pas d’alias (l’parsing d’alias interprocédural est généralement très primitive). De très petits objects struct doivent être passés par valeur et non par référence.

  5. Utilisez autant que possible des tableaux au lieu de pointeurs, en particulier à l’intérieur des boucles (a [i]). Un tableau offre généralement plus d’informations pour l’parsing des alias et après certaines optimisations, le même code sera généré de toute façon (recherchez la réduction de la force de la boucle si vous êtes curieux). Cela augmente également les chances que le mouvement de code invariant en boucle soit appliqué.

  6. Essayez de hisser en dehors des appels en boucle vers de grandes fonctions ou des fonctions externes qui n’ont pas d’effets secondaires (ne dépendez pas de l’itération de la boucle en cours). Dans de nombreux cas, les petites fonctions sont incorporées ou converties en éléments insortingnsèques faciles à lever, mais les grandes fonctions peuvent sembler avoir des effets secondaires lorsque le compilateur ne les utilise 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 le mouvement de code invariant en boucle possible.

  7. Lorsque vous écrivez des tests avec plusieurs conditions, placez les plus probables en premier. si (a || b || c) devrait être if (b || a || c) si b est plus probable que les autres. Les compilateurs ne savent généralement rien des valeurs possibles des conditions et des twigs les plus utilisées (elles peuvent être connues en utilisant des informations de profil, mais peu de programmeurs l’utilisent).

  8. Utiliser un switch est plus rapide que de faire un test comme if (a || b || … || z). Vérifiez d’abord si votre compilateur le fait automatiquement, certains le font et il est plus lisible d’avoir le si si.

Une petite astuce stupide, mais qui vous fera économiser des quantités microscopiques de vitesse et de code.

Toujours transmettre 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éclare pas comme f_2 (x, z, y).

La raison en est que la plate-forme C / C ++ ABI (convention d’appel AKA) promet de transmettre des arguments dans des registres et des emplacements de stack particuliers. Lorsque les arguments sont déjà dans les registres corrects, il n’est pas nécessaire de les déplacer.

En lisant le code désassemblé, j’ai vu des registres ridicules parce que les gens ne suivaient pas cette règle.

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 dynamic . La principale raison pour laquelle je fais cela n’est pas nécessairement la performance, mais cette règle 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 au retour d’un appel à malloc dépend fortement de l’état actuel du tas. Par conséquent, toute fonction qui appelle malloc subira une baisse de performance difficile à prendre en compte. Ce problème de performances peut être minime si le segment de mémoire est toujours propre, mais après que le périphérique s’exécute pendant un certain temps, le tas peut être fragmenté. Les appels vont prendre plus de temps et vous ne pouvez pas calculer facilement la dégradation des performances au fil du temps. Vous ne pouvez pas vraiment produire une estimation de cas pire. L’optimiseur ne peut vous fournir aucune aide dans ce cas non plus. 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, des tranches de glib ) au lieu du segment de mémoire. Les appels d’allocation vont être beaucoup plus rapides et déterministes si vous le faites correctement.

Deux techniques de codage que je n’ai pas vues dans la liste ci-dessus:

Ignorer l’éditeur de liens en écrivant du code en tant que source unique

Bien que la compilation séparée soit très utile pour la compilation, elle est très mauvaise lorsque vous parlez d’optimisation. Fondamentalement, le compilateur ne peut pas optimiser au-delà de l’unité de compilation, c’est-à-dire le domaine réservé à l’éditeur de liens.

Mais si vous concevez bien votre programme, vous pouvez également le comstackr via une source commune unique. C’est-à-dire qu’au lieu de comstackr unit1.c et unit2.c, liez ensuite les deux objects, comstackz all.c, simplement #include unit1.c et unit2.c. Ainsi, vous bénéficierez de toutes les optimisations du compilateur.

It’s very like writing headers only programs in C++ (and even easier to do in C).

This technique is easy enough if you write your program to enable it from the beginning, but you must also be aware it change part of C semantic and you can meet some problems like static variables or macro collision. For most programs it’s easy enough to overcome the small problems that occurs. Also be aware that compiling as an unique source is way slower and may takes huge amount of memory (usually not a problem with modern systems).

Using this simple technique I happened to make some programs I wrote ten times faster!

Like the register keyword, this sortingck could also become obsolete soon. Optimizing through linker begin to be supported by comstackrs gcc: Link time optimization .

Separate atomic tasks in loops

This one is more sortingcky. It’s about interaction between algorithm design and the way optimizer manage cache and register allocation. Quite often programs have to loop over some data structure and for each item perform some actions. Quite often the actions performed can be splitted between two logically independent tasks. If that is the case you can write exactly the same program with two loops on the same boundary performing exactly one task. In some case writing it this way can be faster than the unique loop (details are more complex, but an explanation can be that with the simple task case all variables can be kept in processor registers and with the more complex one it’s not possible and some registers must be written to memory and read back later and the cost is higher than additional flow control).

Be careful with this one (profile performances using this sortingck or not) as like using register it may as well give lesser performances than improved ones.

I’ve actually seen this done in SQLite and they claim it results in performance boosts ~5%: Put all your code in one file or use the preprocessor to do the equivalent to this. This way the optimizer will have access to the entire program and can do more interprocedural optimizations.

Most modern comstackrs should do a good job speeding up tail recursion , because the function calls can be optimized out.

Exemple:

 int fac2(int x, int cur) { if (x == 1) return cur; return fac2(x - 1, cur * x); } int fac(int x) { return fac2(x, 1); } 

Of course this example doesn’t have any bounds checking.

Late Edit

While I have no direct knowledge of the code; it seems clear that the requirements of using CTEs on SQL Server were specifically designed so that it can optimize via tail-end recursion.

Don’t do the same work over and over again!

A common antipattern that I see goes along these lines:

 void Function() { MySingleton::GetInstance()->GetAggregatedObject()->DoSomething(); MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse(); MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool(); MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat(); MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain(); } 

The comstackr actually has to call all of those functions all of the time. Assuming you, the programmer, knows that the aggregated object isn’t changing over the course of these calls, for the love of all that is holy…

 void Function() { MySingleton* s = MySingleton::GetInstance(); AggregatedObject* ao = s->GetAggregatedObject(); ao->DoSomething(); ao->DoSomethingElse(); ao->DoSomethingCool(); ao->DoSomethingReallyNeat(); ao->DoSomethingYetAgain(); } 

In the case of the singleton getter the calls may not be too costly, but it is certainly a cost (typically, “check to see if the object has been created, if it hasn’t, create it, then return it). The more complicated this chain of getters becomes, the more wasted time we’ll have.

  1. Use the most local scope possible for all variable declarations.

  2. Use const whenever possible

  3. Dont use register unless you plan to profile both with and without it

The first 2 of these, especially #1 one help the optimizer analyze the code. It will especially help it to make good choices about what variables to keep in registers.

Blindly using the register keyword is as likely to help as hurt your optimization, It’s just too hard to know what will matter until you look at the assembly output or profile.

There are other things that matter to getting good performance out of code; designing your data structures to maximize cache coherency for instance. But the question was about the optimizer.

Align your data to native/natural boundaries.

I was reminded of something that I encountered once, where the symptom was simply that we were running out of memory, but the result was substantially increased performance (as well as huge reductions in memory footprint).

The problem in this case was that the software we were using made tons of little allocations. Like, allocating four bytes here, six bytes there, etc. A lot of little objects, too, running in the 8-12 byte range. The problem wasn’t so much that the program needed lots of little things, it’s that it allocated lots of little things individually, which bloated each allocation out to (on this particular platform) 32 bytes.

Part of the solution was to put together an Alexandrescu-style small object pool, but extend it so I could allocate arrays of small objects as well as individual items. This helped immensely in performance as well since more items fit in the cache at any one time.

The other part of the solution was to replace the rampant use of manually-managed char* members with an SSO (small-ssortingng optimization) ssortingng. The minimum allocation being 32 bytes, I built a ssortingng class that had an embedded 28-character buffer behind a char*, so 95% of our ssortingngs didn’t need to do an additional allocation (and then I manually replaced almost every appearance of char* in this library with this new class, that was fun or not). This helped a ton with memory fragmentation as well, which then increased the locality of reference for other pointed-to objects, and similarly there were performance gains.

A neat technique I learned from @MSalters comment on this answer allows comstackrs to do copy elision even when returning different objects according to some condition:

 // before BigObject a, b; if(condition) return a; else return b; // after BigObject a, b; if(condition) swap(a,b); return a; 

If you’ve got small functions you call repeatedly, i have in the past got large gains by putting them in headers as “static inline”. Function calls on the ix86 are surprisingly expensive.

Reimplementing recursive functions in a non-recursive way using an explicit stack can also gain a lot, but then you really are in the realm of development time vs gain.

Here’s my second piece of optimisation advice. As with my first piece of advice this is general purpose, not language or processor specific.

Read the comstackr manual thoroughly and understand what it is telling you. Use the comstackr to its utmost.

I agree with one or two of the other respondents who have identified selecting the right algorithm as critical to squeezing performance out of a program. Beyond that the rate of return (measured in code execution improvement) on the time you invest in using the comstackr is far higher than the rate of return in tweaking the code.

Yes, comstackr writers are not from a race of coding giants and comstackrs contain mistakes and what should, according to the manual and according to comstackr theory, make things faster sometimes makes things slower. That’s why you have to take one step at a time and measure before- and after-tweak performance.

And yes, ultimately, you might be faced with a combinatorial explosion of comstackr flags so you need to have a script or two to run make with various comstackr flags, queue the jobs on the large cluster and gather the run time statistics. If it’s just you and Visual Studio on a PC you will run out of interest long before you have sortinged enough combinations of enough comstackr flags.

Cordialement

marque

When I first pick up a piece of code I can usually get a factor of 1.4 — 2.0 times more performance (ie the new version of the code runs in 1/1.4 or 1/2 of the time of the old version) within a day or two by fiddling with comstackr flags. Granted, that may be a comment on the lack of comstackr savvy among the scientists who originate much of the code I work on, rather than a symptom of my excellence. Having set the comstackr flags to max (and it’s rarely just -O3) it can take months of hard work to get another factor of 1.05 or 1.1

When DEC came out with its alpha processors, there was a recommendation to keep the number of arguments to a function under 7, as the comstackr would always try to put up to 6 arguments in registers automatically.

For performance, focus first on writing maintenable code – componentized, loosely coupled, etc, so when you have to isolate a part either to rewrite, optimize or simply profile, you can do it without much effort.

Optimizer will help your program’s performance marginally.

You’re getting good answers here, but they assume your program is pretty close to optimal to begin with, and you say

Assume that the program has been written correctly, comstackd with full optimization, tested and put into production.

In my experience, a program may be written correctly, but that does not mean it is near optimal. It takes extra work to get to that point.

If I can give an example, this answer shows how a perfectly reasonable-looking program was made over 40 times faster by macro-optimization . Big speedups can’t be done in every program as first written, but in many (except for very small programs), it can, in my experience.

After that is done, micro-optimization (of the hot-spots) can give you a good payoff.

i use intel comstackr. on both Windows and Linux.

when more or less done i profile the code. then hang on the hotspots and trying to change the code to allow comstackr make a better job.

if a code is a computational one and contain a lot of loops – vectorization report in intel comstackr is very helpful – look for ‘vec-report’ in help.

so the main idea – polish the performance critical code. as for the rest – priority to be correct and maintainable – short functions, clear code that could be understood 1 year later.

One optimization i have used in C++ is creating a constructor that does nothing. One must manually call an init() in order to put the object into a working state.

This has benefit in the case where I need a large vector of these classes.

I call reserve() to allocate the space for the vector, but the constructor does not actually touch the page of memory the object is on. So I have spent some address space, but not actually consumed a lot of physical memory. I avoid the page faults associated the associated construction costs.

As i generate objects to fill the vector, I set them using init(). This limits my total page faults, and avoids the need to resize() the vector while filling it.

One thing I’ve done is try to keep expensive actions to places where the user might expect the program to delay a bit. Overall performance is related to responsiveness, but isn’t quite the same, and for many things responsiveness is the more important part of performance.

The last time I really had to do improvements in overall performance, I kept an eye out for suboptimal algorithms, and looked for places that were likely to have cache problems. I profiled and measured performance first, and again after each change. Then the company collapsed, but it was interesting and instructive work anyway.

I have long suspected, but never proved that declaring arrays so that they hold a power of 2, as the number of elements, enables the optimizer to do a strength reduction by replacing a multiply by a shift by a number of bits, when looking up individual elements.

Put small and/or frequently called functions at the top of the source file. That makes it easier for the comstackr to find opportunities for inlining.