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


116

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


7
Pourrait être un bon candidat pour le wiki communautaire à mon humble avis car il n'y a pas de réponse définitive `` unique '' à cette (intéressante) question ...
ChristopheD

Ça me manque à chaque fois. Je vous remercie de le faire remarquer.
EvilTeach

Par «mieux», vous entendez simplement «plus vite» ou avez-vous d'autres critères d'excellence en tête?
High Performance Mark

1
Il est assez difficile d'écrire un bon allocateur de registre, en particulier de manière portable, et l'allocation de registre est absolument essentielle pour les performances et la taille du code. registerrendu le code sensible aux performances plus portable en combattant les mauvais compilateurs.
Potatoswatter

1
@EvilTeach: wiki communautaire ne signifie pas "pas de réponse définitive", ce n'est pas synonyme de balise subjective. Le wiki de la communauté signifie que vous souhaitez céder votre message à la communauté afin que d'autres personnes puissent le modifier. Ne vous sentez pas obligé de rédiger vos questions par wiki si vous n'en avez pas envie.
Juliet

Réponses:


54

É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 à

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[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 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:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

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


7
Cela a l'avantage supplémentaire de rendre souvent les choses plus faciles à lire / comprendre pour les programmeurs, car ils n'ont pas non plus à se soucier d'éventuels effets secondaires non évidents.
Michael Burr

La plupart des IDE affichent des variables locales par défaut, il y a donc moins de frappe
EvilTeach

9
vous pouvez également activer cette optimisation en utilisant des pointeurs restreints
Ben Voigt

4
@Ben - c'est vrai, mais je pense que cette façon est plus claire. De plus, si l'entrée et la sortie se chevauchent, je pense que le résultat n'est pas spécifié avec des pointeurs restreints (obtiennent probablement un comportement différent entre le débogage et la publication), alors que cette manière sera au moins cohérente. Ne vous méprenez pas, j'aime utiliser restrict, mais j'aime ne pas en avoir encore plus besoin.
celion

Vous devez juste espérer que Foo n'a pas défini d'opération de copie qui copie quelques méga de données ;-)
Skizz

76

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.


20
Les développeurs Compiier ont un temps limité, comme tout le monde. Toutes les optimisations ne seront pas intégrées au compilateur. Comme &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.
Dave Jarvis

22
& and% est à peu près toujours optimisé, ainsi que la plupart des autres astuces arithmétiques bon marché. Ce qui n'est pas optimisé, c'est le cas de l'opérande de droite étant une variable qui se trouve être toujours une puissance de deux.
Potatoswatter

8
Pour clarifier, il me semble avoir dérouté certains lecteurs: le conseil dans la pratique de codage que je propose est de d'abord développer un code simple qui n'utilise pas les instructions de mise en page de la mémoire pour établir une base de performance. Ensuite, essayez les choses une par une et mesurez leur impact. Je n'ai donné aucun conseil sur la performance des opérations.
High Performance Mark

17
Pour une puissance de deux constante n, gcc remplace % npar & (n-1) même lorsque l'optimisation est désactivée . Ce n'est pas exactement "rarement, voire jamais" ...
Porculus

12
% NE PEUT PAS être optimisé au fur et à mesure que le type est signé, en raison des règles idiotes de C pour la division entière négative (arrondir vers 0 et avoir un reste négatif, plutôt que d'arrondir vers le bas et toujours avoir un reste positif). Et la plupart du temps, les codeurs ignorants utilisent des types signés ...
R .. GitHub STOP HELPING ICE

47

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:

#define N 1000000;
int matrix[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];
  }
}

Il ne s'agit pas à proprement parler d'un problème d'optimisation, mais d'un problème d'optimisation.
EvilTeach

10
Bien sûr, c'est un problème d'optimisation. Les gens rédigent des articles sur l'optimisation automatique des échanges de boucles depuis des décennies.
Phil Miller

20
@Potatoswatter De quoi parlez-vous? Le compilateur C peut faire ce qu'il veut tant que le même résultat final est observé, et en effet GCC 4.4 a -floop-interchangece qui va inverser une boucle interne et externe si l'optimiseur le juge rentable.
éphémère le

2
Euh, bien voilà. La sémantique C est souvent entachée de problèmes d'aliasing. Je suppose que le vrai conseil ici est de passer ce drapeau!
Potatoswatter

36

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 inlinemacros ou

Chaque 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 #definemacros ou de inlinefonctions. (Oui, je sais que ce inlinen'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:

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

Après déroulement:

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 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 compilateur memcpy. - TM

Réduction des ifdéclarations

Les 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 ifinstructions en affectations booléennes. Certains processeurs peuvent exécuter des instructions de manière conditionnelle sans branchement:

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

Le court-circuit de l' opérateur ET logique ( &&) empêche l'exécution des tests si le statusest 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 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:

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 printf famille pour des données constantes

Les 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 chartableau en utilisant plusieurs sprintf, puis utilisez fwrite. 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 le staticpré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.


2
Intéressant pouvez-vous donner un exemple où vous avez obtenu un meilleur code avec quelques petites instructions, au lieu d'une plus grande. Pouvez-vous montrer un exemple de réécriture d'un if, en utilisant des booléens. En général, je laisserais la boucle se dérouler au compilateur, car il a probablement une meilleure idée de la taille du cache. Je suis un peu surpris par l'idée de sprintfing, puis d'écrire. Je pense que fprintf fait cela sous le capot. Pouvez-vous donner un peu plus de détails ici?
EvilTeach

1
Il n'y a aucune garantie que les fprintfformats dans un tampon séparé sortent ensuite le tampon. Un rationalisé (pour l'utilisation de la mémoire) fprintfproduirait 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.
Thomas Matthews

3
Une fois, j'ai eu une amélioration significative des performances en enroulant une boucle. Ensuite, j'ai compris comment le resserrer davantage en utilisant une certaine indirection, et le programme est devenu nettement plus rapide. (Le profilage a montré que cette fonction particulière représentait 60 à 80% du temps d'exécution, et j'ai testé les performances avec soin avant et après.) Je pense que l'amélioration était due à une meilleure localisation, mais je n'en suis pas tout à fait sûr.
David Thornley

16
Beaucoup d'entre elles sont des optimisations de programmeur plutôt que des moyens pour les programmeurs d'aider le compilateur à l'optimisation, ce qui était le sens de la question initiale. Par exemple, le déroulement de la boucle. Oui, vous pouvez faire votre propre déroulement, mais je pense qu'il est plus intéressant de déterminer quels sont les obstacles au déroulement du compilateur pour vous et à les supprimer.
Adrian McCarthy

26

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.


14
C'est une bonne réponse, mais notez que l'optimisation de l'ensemble du programme est de plus en plus populaire et peut en fait intégrer des fonctions dans les unités de traduction.
Phil Miller

1
@Novelocrat Oui - inutile de dire que j'ai été très surpris la première fois que j'ai vu quelque chose de A.cse mettre en ligne B.c.
Jonathon Reinhart

18

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


4
+1 pour les load-hit-stores et différents types de registres. Je ne sais pas à quel point c'est important sur x86, mais ils sont dévastateurs sur PowerPC (par exemple Xbox360 et Playstation3).
celion

La plupart des articles sur les techniques d'optimisation des boucles du compilateur supposent une imbrication parfaite, ce qui signifie que le corps de chaque boucle, à l'exception de la plus interne, n'est qu'une autre boucle. Ces articles ne discutent tout simplement pas des étapes nécessaires pour généraliser une telle situation, même s'il est très clair qu'elles peuvent l'être. Ainsi, je m'attendrais à ce que beaucoup d'implémentations ne prennent pas en charge ces généralisations, en raison de l'effort supplémentaire impliqué. Ainsi, les nombreux algorithmes d'optimisation de l'utilisation du cache dans les boucles peuvent fonctionner beaucoup mieux sur des nids parfaits que sur des nids imparfaits.
Phil Miller

11

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


7
J'avoue être particulier - je travaille sur de grands codes scientifiques de calcul de nombre qui sont liés à la bande passante mémoire. Pour la population générale des programmes, je suis d'accord avec Neil.
High Performance Mark

6
Vrai; mais une grande partie de ce code lié aux E / S est de nos jours écrit dans des langages qui sont pratiquement des pessimiseurs - des langages qui n'ont même pas de compilateurs. Je soupçonne que les zones où C et C ++ sont encore utilisés auront tendance à être des zones où il est plus important d'optimiser quelque chose (utilisation du processeur, utilisation de la mémoire, taille du code ...)
Porculus

3
J'ai passé la plupart des 30 dernières années à travailler sur du code avec très peu d'E / S. Économisez 2 ans en faisant des bases de données. Graphiques, systèmes de contrôle, simulation - rien de tout cela n'est lié aux E / S. Si les E / S étaient le goulot d'étranglement de la plupart des gens, nous ne porterions pas beaucoup d'attention à Intel et AMD.
phkahler

2
Oui, je n'approuve pas vraiment cet argument, sinon nous (à mon travail) ne chercherions pas des moyens de passer plus de temps de calcul à faire également des E / S. De plus, la plupart des logiciels liés aux E / S que j'ai rencontrés ont été liés aux E / S parce que les E / S ont été effectuées de manière négligente; si l'on optimise les modèles d'accès (comme avec la mémoire), on peut obtenir d'énormes gains de performances.
dash-tom-bang

3
J'ai récemment découvert que presque aucun code écrit en langage C ++ n'est lié aux E / S. Bien sûr, si vous appelez une fonction du système d'exploitation pour le transfert de disque en masse, votre thread peut entrer en attente d'E / S (mais avec la mise en cache, même cela est discutable). Mais les fonctions habituelles de la bibliothèque d'E / S, celles que tout le monde recommande parce qu'elles sont standard et portables, sont en fait misérablement lentes par rapport à la technologie de disque moderne (même les choses à prix modéré). Très probablement, les E / S ne constituent le goulot d'étranglement que si vous videz complètement le disque après avoir écrit quelques octets. OTOH, l'interface utilisateur est une autre affaire, nous les humains sommes lents.
Ben Voigt

11

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:

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

8
Pas beaucoup, rarement. Cela améliore cependant l'exactitude réelle.
Potatoswatter

5
En C et C ++, le compilateur ne peut pas utiliser const pour optimiser, car le rejeter est un comportement bien défini.
dsimcha

+1: const est un bon exemple de quelque chose qui aura un impact direct sur le code compilé. Commentaire de re @ dsimcha - un bon compilateur testera pour voir si cela se produit. Bien sûr, un bon compilateur "trouvera" des éléments const qui ne sont pas déclarés ainsi de toute façon ...
Hogan

@dsimcha: La modification d'un pointeur qualifié const et restrict n'est cependant pas définie. Un compilateur pourrait donc optimiser différemment dans un tel cas.
Dietrich Epp

6
@dsimcha rejetant constune constréférence ou un constpointeur vers un non- constobjet est bien défini. modifier un constobjet réel (c'est-à-dire un objet déclaré comme à l' constorigine) ne l'est pas.
Stephen Lin

9

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:

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 une comparaison dans les tests de portée.

#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 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:

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


2
Il n'est en fait pas nécessaire d'utiliser la soustraction lors du test des plages, LLVM, GCC et mon compilateur le font au moins automatiquement. Peu de gens comprendraient probablement ce que fait le code avec soustraction et encore moins pourquoi il fonctionne réellement.
Gratian Lup

dans l'exemple ci-dessus, b () ne peut pas être appelé car si (x <0) alors a () sera appelé.
EvilTeach

@EvilTeach Non, ce ne sera pas le cas. La comparaison qui aboutit à l'appel à a () est! X
nategoose

@nategoose. si x est -3 alors! x est vrai.
EvilTeach

@EvilTeach In C 0 est faux et tout le reste est vrai, donc -3 est vrai, donc! -3 est faux
nategoose

9

J'ai écrit un compilateur d'optimisation C et voici quelques éléments très utiles à considérer:

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

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

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

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

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

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

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


7

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.


Ma règle de base est que si vous devez allouer dynamiquement, obtenez un tableau pour ne pas avoir à le refaire. Préallouez-les vecteurs.
EvilTeach

7

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.


2
Ni C ni C ++ ne donnent de garanties sur, ni même ne mentionnent, le passage de registres ou d'emplacements de pile particuliers. C'est l' ABI (par exemple Linux ELF) qui détermine les détails du passage des paramètres.
Emmet

5

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.


2
Oui, à présent, LTO a rendu la première moitié de ce message redondante et probablement un mauvais conseil.
underscore_d

@underscore_d: il y a encore quelques problèmes (principalement liés à la visibilité des symboles exportés), mais du simple point de vue des performances, il n'y en a probablement plus.
kriss

4

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.


5
Le fait de regrouper les fonctions utilisées à proximité physique de la source augmente la probabilité qu'elles soient proches les unes des autres dans les fichiers objet et proches les unes des autres dans votre exécutable. Cette localisation améliorée des instructions peut aider à éviter les échecs du cache d'instructions lors de l'exécution.
paxos1977

Le compilateur AIX a un commutateur de compilateur pour encourager ce comportement -qipa [= <suboptions_list>] | -qnoipa Active ou personnalise une classe d'optimisations appelée analyse interprocédurale (IPA).
EvilTeach

4
Le mieux est d'avoir un moyen de se développer qui n'exige pas cela. Utiliser ce fait comme excuse pour écrire du code non modulaire entraînera globalement un code lent et présentant des problèmes de maintenance.
Hogan

3
Je pense que cette information est légèrement datée. En théorie, les fonctionnalités d'optimisation de l'ensemble du programme intégrées à de nombreux compilateurs maintenant (par exemple, "Link-time Optimization" dans gcc) offrent les mêmes avantages, mais avec un flux de travail totalement standard (plus des temps de recompilation plus rapides que de tout mettre dans un seul fichier !)
Ponkadoodle

@Wallacoloo Pour sûr, c'est faaar outta date. FWIW, je viens d'utiliser le LTO de GCC pour la première fois aujourd'hui, et - toutes choses égales par ailleurs -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.)
underscore_d

4

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:

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);
}

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.


1
la question est à propos de C. C ne supprime pas la récursivité de queue, donc queue ou autre récursivité, la pile pourrait exploser si la récursivité va trop loin.
Toad

1
J'ai évité le problème de la convention d'appel, en utilisant un goto. Il y a moins de frais généraux de cette façon.
EvilTeach

2
@hogan: c'est nouveau pour moi. Pourriez-vous indiquer un compilateur qui fait cela? Et comment pouvez-vous être sûr qu'il l'optimise réellement? S'il le faisait, il fallait vraiment être sûr qu'il le fasse. Ce n'est pas quelque chose que vous espérez que l'optimiseur du compilateur reprendra (comme l'inlining qui peut ou non fonctionner)
Toad

6
@hogan: Je suis corrigé. Vous avez raison de dire que Gcc et MSVC font tous deux une optimisation de la récursivité de queue.
Toad

5
Cet exemple n'est pas une récursion de queue car ce n'est pas l'appel récursif qui est le dernier, c'est la multiplication.
Brian Young

4

Ne faites pas le même travail encore et encore!

Un anti-modèle commun que je vois va dans ce sens:

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

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

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

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.


3
  1. Utilisez la portée la plus locale possible pour toutes les déclarations de variables.

  2. Utilisez constautant que possible

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



3

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.


3

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:

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

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

2

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 conversion de la récursivité en pile est une optimisation supposée sur ompf.org, pour les personnes développant des raytracers et écrivant d'autres algorithmes de rendu.
Tom

... Je devrais ajouter à cela, que la plus grande surcharge de mon projet de raytracer personnel est la récursivité basée sur une table virtuelle à travers une hiérarchie de volumes englobants utilisant le modèle composite. Il ne s'agit en réalité que d'un ensemble de boîtes imbriquées structurées sous forme d'arbre, mais l'utilisation du modèle provoque un gonflement des données (pointeurs de table virtuels) et réduit la cohérence des instructions (ce qui pourrait être une boucle petite / étroite est maintenant une chaîne d'appels de fonction)
Tom

2

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


2

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.


Les bits x86-64 autorisent également de nombreux paramètres transmis par registre, ce qui peut avoir un effet dramatique sur la surcharge des appels de fonction.
Tom

1

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.


3
Cela ne fonctionne que si les "interfaces" de couplage sont elles-mêmes susceptibles d'optimisation. Une interface peut être intrinsèquement «lente», par exemple en forçant des recherches ou des calculs redondants, ou en forçant un mauvais accès au cache.
Tom

1

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

Supposons que le programme a été écrit correctement, compilé avec une optimisation complète, testé et mis en production.

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.


1

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.


Vous êtes sur le point de répondre à la question ... Quelles sortes de choses faites-vous au code, pour permettre au compilateur de faire ce genre d'optimisations?
EvilTeach

1
Essayer d'écrire plus en style C (vs en C ++) par exemple en évitant les fonctions virtuelles sans besoin absolu, surtout si elles vont être appelées souvent, évitez les AddRefs .. et tous les trucs sympas (encore une fois à moins que cela ne soit vraiment nécessaire). Ecrire du code facile à insérer - moins de paramètres, moins de "si" -s. Ne pas utiliser de variables globales sauf en cas de nécessité absolue. Dans la structure de données - placez les champs plus larges en premier (double, int64 passe avant int) - donc le compilateur aligne la structure sur la taille naturelle du premier champ - s'aligne bien pour perf.
jf.

1
La disposition et l'accès aux données sont absolument essentiels pour les performances. Donc après le profilage - je casse parfois une structure en plusieurs suivant la localité des accès. Une astuce plus générale - utilisez int ou size-t contre char - même les valeurs de données sont petites - évitez diverses performances. pénalités stocker pour charger le blocage, les problèmes avec les calages partiels des registres. bien sûr, cela ne s'applique pas lorsque vous avez besoin de grands tableaux de telles données.
jf.

Encore un - évitez les appels système, à moins qu'il n'y ait un réel besoin :) - ils sont TRÈS chers
jf.

2
@jf: J'ai fait +1 sur votre réponse, mais pourriez-vous déplacer la réponse des commentaires vers le corps de la réponse? Ce sera plus facile à lire.
kriss

1

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.


6
Je crois qu'une implémentation typique de std :: vector ne construit pas réellement plus d'objets lorsque vous réservez () plus de capacité. Il alloue simplement des pages. Les constructeurs sont appelés plus tard, en utilisant placement new, lorsque vous ajoutez réellement des objets au vecteur - ce qui est (vraisemblablement) juste avant d'appeler init (), vous n'avez donc pas vraiment besoin de la fonction init () séparée. Souvenez-vous également que même si votre constructeur est "vide" dans le code source, le constructeur compilé peut contenir du code pour initialiser des choses comme des tables virtuelles et RTTI, de sorte que les pages sont de toute façon touchées au moment de la construction.
Wyzard

1
Oui. Dans notre cas, nous utilisons push_back pour remplir le vecteur. Les objets n'ont pas de fonctions virtuelles, ce n'est donc pas un problème. La première fois que nous l'avons essayé avec le constructeur, nous avons été étonnés par le volume de défauts de page. J'ai réalisé ce qui s'était passé, et nous avons arraché les tripes du constructeur, et le problème de défaut de page a disparu.
EvilTeach

Cela me surprend un peu. Quelles implémentations C ++ et STL utilisiez-vous?
David Thornley

3
Je suis d'accord avec les autres, cela ressemble à une mauvaise implémentation de std :: vector. Même si vos objets avaient des vtables, ils ne seraient pas construits avant votre push_back. Vous devriez pouvoir tester cela en déclarant que le constructeur par défaut est privé, car tout vecteur aura besoin du constructeur de copie pour push_back.
Tom

1
@David - L'implémentation était sous AIX.
EvilTeach

1

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.


0

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.


6
C'était vrai, de nos jours, ça l'est plus. En fait, exactement le contraire est vrai. Si vous déclarez vos tableaux avec des puissances de deux, vous vous retrouverez très probablement dans la situation où vous travaillez sur deux pointeurs séparés par une puissance de deux en mémoire. Le problème est que les caches du processeur sont organisés comme ça et que vous risquez de vous retrouver avec les deux tableaux qui se battent autour d'une ligne de cache. Vous obtenez des performances horribles de cette façon. Avoir l'un des pointeurs à quelques octets d'avance (par exemple, pas de puissance de deux) empêche cette situation.
Nils Pipenbrinck

+1 Nils, et une occurrence spécifique de ceci est "aliasing 64k" sur le matériel Intel.
Tom

C'est quelque chose qui est facilement réfuté en regardant le démontage, d'ailleurs. J'ai été étonné, il y a des années, de voir comment gcc optimiserait toutes sortes de multiplications constantes avec des changements et des ajouts. Par exemple, val * 7transformé en ce qui ressemblerait autrement (val << 3) - val.
dash-tom-bang

0

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.


Vraiment? Pouvez-vous citer une justification et des exemples pour cela? Ne pas dire que ce n'est pas vrai, mais cela semble peu intuitif.
underscore_d

@underscore_d il ne peut pas insérer quelque chose tant que la définition de la fonction n'est pas connue. Bien que les compilateurs modernes puissent effectuer plusieurs passes pour que la définition soit connue au moment de la génération du code, je ne le suppose pas.
Mark Ransom

J'avais supposé que les compilateurs travaillaient sur des graphes d'appels abstraits plutôt que sur un ordre de fonction physique, ce qui signifie que cela n'aurait pas d'importance. Bien sûr, je suppose que cela ne fait pas de mal d'être très prudent - surtout quand, à part les performances, l'OMI, il semble juste plus logique de définir les fonctions qui sont appelées avant celles qui les appellent. Je devrais tester les performances mais je serais surpris si cela comptait, mais d'ici là, je suis ouvert à la surprise!
underscore_d
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.