web-dev-qa-db-fra.com

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. Comme solution de contournement, K&R a inventé le mot clé register , pour faire comprendre 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 dans la mesure où 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é de registre est devenu sans importance.

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

Quelles pratiques de codage sont disponibles pour permettre au compilateur/optimiseur de générer du code plus rapidement?

  • L'identification de la plateforme et du compilateur que vous utilisez serait appréciée.
  • Pourquoi la technique semble-t-elle fonctionner?
  • Un exemple de code est recommandé.

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 qui supprimera ces interdictions et permettra à l'optimiseur de générer du code encore plus rapide?

[Modifier] Lien relatif au décalage

116
EvilTeach

Écrivez dans des variables locales et non dans des arguments de sortie! Cela peut être très utile 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<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 peut paraître 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 aucun des arguments. Cela peut vous aider à éviter le redoutable magasin de charge (mentionné par Francis Boivin dans ce fil).

54
celion

Voici une pratique de codage pour aider le compilateur à créer du code rapide - n'importe quel langage, n'importe quelle plate-forme, tout compilateur, tout problème:

Faites pas utilisez des astuces astucieuses qui forcent, voire encouragent, le compilateur à disposer les variables en mémoire (y compris le cache et les registres) comme vous le pensez le mieux. Écrivez d'abord un programme correct et maintenable.

Ensuite, profilez votre code.

Ensuite, et seulement à ce moment-là, vous voudrez peut-être commencer à enquêter sur 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 à 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 le compte d'une "astuce" pour obtenir de meilleures performances du code, gardez à l'esprit que les rédacteurs du compilateur ont également lu à ce sujet et, si cela vaut la peine, probablement mis en œuvre. Ils ont probablement écrit ce que vous avez lu en premier lieu.

73

L'ordre dans lequel vous parcourez la mémoire peut avoir de profondes répercussions sur les performances et les compilateurs ne sont pas vraiment bons pour comprendre cela et le corriger. Vous devez être conscients 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 de ligne principale. La traversée de tableaux dans le format principal de colonne aura tendance à vous faire plus de ratés 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];
  }
}
47
vicatcu

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éclarez les petites fonctions comme inline ou macros

Chaque appel à une fonction (ou méthode) entraîne des frais généraux, tels que la transmission 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'inline, que ce soit comme #define macros ou inline fonctions. (Oui, je sais que 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 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 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émentation, vérification, démarrage de la boucle) et divisez 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 de boucle Avant:

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

Après le 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: plus d'instructions sont exécutées avant que le processeur ne doive recharger le cache d'instructions.

J'ai eu des résultats incroyables lorsque j'ai déroulé une boucle de 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 blocs a amélioré les performances de 1 heure à 5 minutes. Le déroulement des boucles a également fourni d'excellentes performances en langage assembleur, mon memcpy était beaucoup plus rapide que le memcpy du compilateur. - T.M.

Réduction des instructions if

Les processeurs détestent les branches ou les sauts, car ils forcent le processeur à recharger sa file d'attente d'instructions.

Arithmétique booléenne (Modifié: format de code appliqué au fragment de code, exemple ajouté)

Convertissez les instructions if en affectations booléennes. Certains processeurs peuvent exécuter conditionnellement des instructions sans branchement:

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

Le court-circuitage de l'opérateur ET logique (&&) empêche l'exécution des tests si 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 variable de facteurs en dehors des boucles

Si une variable est créée à la volée à l'intérieur d'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.

Expressions factorielles constantes en dehors des boucles

Si un calcul ou une valeur de variable ne dépend pas de l'index de boucle, déplacez-le à l'extérieur (avant) de la boucle.

E/S en blocs

Lire et écrire des données en gros morceaux (blocs). Le plus gros le meilleur. Par exemple, la lecture d'un octet à la fois est moins efficace que la lecture de 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 famille printf pour des données constantes

Des données constantes peuvent être sorties en utilisant une écriture de bloc. L'écriture formatée perdra du temps à analyser le texte pour rechercher des caractères de formatage ou à traiter des commandes de formatage. Voir l'exemple de code ci-dessus.

Formater dans la mémoire, puis écrire

Formatez en un tableau char en utilisant plusieurs sprintf, puis utilisez fwrite. Cela permet également de diviser la disposition des données en "sections constantes" et en sections variables. Pensez à fusion et publipostage .

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

Lorsque des variables sont déclarées sans 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 corrigé en utilisant le préfixe static.

Enfin, le code comme le compilateur

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 aide également. Si je veux que le compilateur utilise des instructions de transfert de bloc spéciales, j'écrirai du code qui semble devoir utiliser les instructions spéciales.

36
Thomas Matthews

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

Cela dit, vous ne devriez pas faire de boucle interne sur une petite fonction d'un fichier dans un autre fichier, car cela l'empêche d'être insérée.

Évitez si possible de prendre l'adresse d'une variable. 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 les registres si vous évitez les pointeurs - c'est essentiel pour la vectorisation.

Ce qui nous amène au point suivant, lire le manuel ^ # $ @! GCC peut vectoriser du code C simple si vous saupoudrez un __restrict__ Ici et une __attribute__( __aligned__ ) là. Si vous voulez quelque chose de très spécifique de l'optimiseur, vous devrez peut-être être spécifique.

26
Potatoswatter

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 de mémoire et é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é un "alias", vous êtes dans une bonne session de débogage de comportements non définis!

Cache-miss: Je ne sais pas vraiment comment vous pouvez aider le compilateur car il est principalement algorithmique, mais il existe des éléments intrinsèques pour la prélecture de la mémoire.

N'essayez pas non plus de convertir des valeurs à virgule flottante en int et vice versa, car ils utilisent différents registres 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é. .

18
Francis Boivin

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 aucun sens sans que l'optimiseur soit activé.

11
anon

utilisez autant que possible la const const dans votre code. Il permet au compilateur d'optimiser beaucoup mieux.

Dans ce document, vous trouverez de nombreux autres conseils d'optimisation: 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 par allocation de classe
  • considérer les allocateurs de conteneurs stl
  • l'optimisation du "membre vide"
  • etc
11
Toad

Essayez de programmer autant que possible en utilisant une seule affectation statique. 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 que la plupart des compilateurs convertissent votre code pour faire leurs optimisations car il est plus facile de travailler avec. En faisant cela, les endroits où le compilateur pourrait être confus sont mis en évidence. 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ù provient une variable car il n'y a 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 par rapport à 0 que la plupart des processeurs vous donnent lors d'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 plage.

#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 un court-circuit sur les expressions booléennes et évite au compilateur d'avoir à essayer de comprendre comment gérer le résultat du premier comparatif tout en effectuant la deuxième puis en les combinant. 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 le faites, le rc n'est pas encore utilisé, il peut donc y aller.

Lorsque vous utilisez les fonctions de chaîne en 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 en la récupérant simplement du 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. Les compilateurs ne sont pas si doués pour capter 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 vous n'avez qu'un seul point de retour.

(astuces dont je me suis souvenu 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 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 lesquels les fonctions appelées s'attendent généralement à ce que leurs paramètres soient (il doit s'écarter à la fois de la fonction appelée et de l'emplacement de tous les appelants pour ce faire). Le compilateur peut également souvent profiter 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 variables qui se trouvent dans les registres ou les emplacements de mémoire que la fonction appelée ne dérange pas. Cela fonctionne particulièrement bien lorsqu'il y a peu d'appels à une fonction. Cela obtient une grande partie de l'avantage du code en ligne, mais sans réellement en ligne.

9
nategoose

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 constante interprocédurale et à l'analyse d'alias de faire son travail, sinon le compilateur doit présumer que la fonction peut être appelée de 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 statiques et constantes si possible. S'ils sont initialisés une fois (en lecture seule), il est préférable d'utiliser une liste d'initialiseurs comme la constante statique 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 échouera à remplacer 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. N'utilisez les paramètres du pointeur que si nécessaire et marquez-les comme restreints si possible. Cela aide beaucoup l'analyse d'alias car le programmeur garantit qu'il n'y a pas d'alias (l'analyse d'alias interprocédural 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 dans la mesure du possible, en particulier à l'intérieur des boucles (a [i]). Un tableau offre généralement plus d'informations pour l'analyse des alias et après quelques optimisations, le même code sera quand même généré (recherchez la réduction de la force de la boucle si vous êtes curieux). Cela augmente également les chances d'application d'un mouvement de code invariant en boucle.

  6. Essayez de hisser en dehors des appels de boucle vers de grandes fonctions ou des fonctions externes qui n'ont pas d'effets secondaires (ne dépendent pas de l'itération de boucle actuelle). Dans de nombreux cas, les petites fonctions sont intégrées ou converties en éléments intrinsèques qui sont faciles à hisser, mais les grandes fonctions peuvent sembler au compilateur avoir des effets secondaires alors qu'elles ne le font pas réellement. Les effets secondaires des 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, ce qui rend 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) devrait être if (b || a || c) if b est plus susceptible d'être vrai que les autres. Les compilateurs ne savent généralement rien des valeurs possibles des conditions et des branches qui sont prises le plus (elles peuvent être connues en utilisant les informations de profil, mais peu de programmeurs les 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 cependant if.

9
Gratian Lup

Dans le cas de systèmes embarqués et de code écrit en C/C++, j'essaye d'éviter allocation dynamique de mémoire autant que possible. 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 qu'il faut 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 un impact sur les performances qui ne peut pas être facilement pris en compte. Cet impact sur les performances peut être minime si le segment de mémoire est toujours propre, mais après que l'appareil fonctionne pendant un certain temps, le segment de mémoire peut devenir fragmenté. Les appels vont prendre plus de temps et vous ne pouvez pas facilement calculer la dégradation des performances au fil du temps. Vous ne pouvez pas vraiment produire une estimation de cas pire. L'optimiseur ne peut pas non plus vous aider dans ce cas. Pour aggraver les choses, si le tas devient trop fortement 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 vont être beaucoup plus rapides et déterministes si vous le faites correctement.

7
figurassa

Une petite astuce stupide, mais qui vous permettra d'é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 registres corrects, il n'est pas nécessaire de les déplacer.

En lisant le code désassemblé, j'ai vu des registres ridicules mélanger car les gens n'ont pas suivi cette règle.

7
Zan Lynx

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

Contourner l'éditeur de liens en écrivant du code en tant que source unique

Bien que la compilation séparée soit vraiment agréable 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é aux éditeurs de liens.

Mais si vous concevez bien votre programme, vous pouvez également le compiler via une source commune unique. C'est au lieu de compiler unit1.c et unit2.c puis liez les deux objets, compilez all.c qui ne fait que #inclure unit1.c et unit2.c. Vous bénéficierez ainsi de toutes les optimisations du compilateur.

C'est un peu comme écrire des en-têtes uniquement des programmes en C++ (et encore plus facile à faire en C).

Cette technique est assez facile si vous écrivez votre programme pour l'activer depuis le début, mais vous devez également être conscient qu'il change une partie de la sémantique C et vous pouvez rencontrer des problèmes comme les variables statiques ou la macro collision. Pour la plupart des programmes, il est assez facile de surmonter les petits problèmes qui se produisent. Sachez également que la compilation en tant que source unique est beaucoup plus lente et peut nécessiter 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 faire des programmes que j'ai écrits dix fois plus vite!

Comme le mot-clé de registre, 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 .

Tâches atomiques séparées en boucles

Celui-ci est plus délicat. Il s'agit de l'interaction entre la conception d'algorithmes et la façon dont l'optimiseur gère le cache et l'allocation des registres. Très souvent, les programmes doivent parcourir une certaine 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 frontière 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 certains les registres doivent être écrits en mémoire et relus plus tard et le coût est plus élevé que le contrôle de flux supplémentaire).

Soyez prudent avec celui-ci (performances de profil en utilisant cette astuce ou non) car comme utiliser le registre, il peut aussi donner des performances moindres que celles améliorées.

5
kriss

La plupart des compilateurs modernes devraient faire un bon travail en accélérant 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.

Édition tardive

Bien que je n'aie aucune connaissance directe du code; il semble clair que les exigences d'utilisation des CTE sur SQL Server ont été spécifiquement conçues pour pouvoir être optimisées via la récursivité de fin.

4
Hogan

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

Un antipattern 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, sachiez que l'objet agrégé ne change pas au cours de ces appels, par amour pour 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 peuvent ne pas être trop coûteux, mais il s'agit certainement d'un coût (généralement, "vérifiez si l'objet a été créé, s'il ne l'a pas été, créez-le, puis renvoyez-le). plus cette chaîne de getters devient compliquée, plus nous perdrons de temps.

4
dash-tom-bang

J'ai en fait vu cela dans SQLite et ils prétendent que cela entraîne une augmentation des performances ~ 5%: Mettez tout votre code dans un fichier ou utilisez le préprocesseur pour faire l'équivalent. De cette façon, l'optimiseur aura accès à l'ensemble du programme et pourra effectuer plus d'optimisations interprocédurales.

4
dsimcha
  1. Utilisez l'étendue la plus locale possible pour toutes les déclarations de variables.

  2. Utilisez const autant que possible

  3. Dont utilisez le registre sauf si vous prévoyez de profiler avec et sans

Les 2 premiers d'entre eux, en particulier le premier, aident l'optimiseur à analyser le code. Cela l'aidera notamment à faire de bons choix sur les variables à conserver dans les registres.

Utiliser aveuglément le mot-clé de registre est aussi susceptible d'aider que de nuire à votre optimisation.

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 portait sur l'optimiseur.

3
John Knoeller

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 lors du retour de 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;
3
Xeo

Je me suis 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 a été 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. Par exemple, 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 tellement 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 pool de petits objets de style Alexandrescu, mais à l'étendre afin que je puisse allouer des tableaux de petits objets ainsi que des éléments individuels. Cela a également énormément amélioré les performances, car davantage d'éléments peuvent être placés dans le cache à tout moment.

L'autre partie de la solution consistait à remplacer l'utilisation généralisée des membres char * gérés manuellement par une chaîne SSO (optimisation des petites chaînes). L'allocation minimale étant de 32 octets, j'ai construit une classe de chaînes qui avait un tampon intégré de 28 caractères 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 aidé une tonne de 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, il y a eu des gains de performances.

3
dash-tom-bang
3
EvilTeach

Si vous avez de petites fonctions que vous appelez à plusieurs reprises, j'ai dans le passé obtenu de gros gains en les mettant dans des en-têtes comme "statiques en ligne". Les appels de fonction sur le ix86 sont étonnamment chers.

Réimplémenter des fonctions récursives de manière non récursive en utilisant une pile explicite peut également gagner beaucoup, mais vous êtes vraiment dans le domaine du temps de développement vs gain.

2
Remy

Voici mon deuxième conseil d'optimisation. Comme pour mon premier conseil, il s'agit d'un usage 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é la sélection du bon algorithme comme essentielle pour retirer les performances d'un programme. Au-delà de cela, le taux de retour (mesuré en amélioration de l'exécution de code) sur le temps que vous investissez dans l'utilisation du compilateur est beaucoup plus élevé que le taux de retour dans l'ajustement du code.

Oui, les auteurs 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 rend parfois les choses plus lentes. C'est pourquoi vous devez prendre une étape à la fois et mesurer les performances avant et après Tweak.

Et oui, en fin de compte, vous pourriez être confronté à une explosion combinatoire d'indicateurs de compilation, vous devez donc avoir un script ou deux pour exécuter make avec divers indicateurs de compilation, mettre en file d'attente les travaux sur le grand cluster et rassembler les statistiques d'exécution. Si c'est juste vous et Visual Studio sur un PC, vous manquerez d'intérêt bien avant d'avoir essayé suffisamment de combinaisons de drapeaux 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) dans un jour ou deux en jouant avec les drapeaux du compilateur. Certes, cela peut être un commentaire sur le manque de connaissance des compilateurs parmi les scientifiques à 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 drapeaux du compilateur sur max (et c'est rarement juste -O3), cela peut prendre des mois de dur labeur pour obtenir un autre facteur de 1,05 ou 1,1

2

Lorsque DEC est sorti avec ses processeurs alpha, il a été recommandé de garder le nombre d'arguments d'une fonction sous 7, car le compilateur essayait toujours de mettre jusqu'à 6 arguments dans les registres automatiquement.

2
EvilTeach

Une chose que j'ai faite est d'essayer de limiter les actions coûteuses aux endroits où l'utilisateur peut s'attendre à ce que le programme soit un peu retardé. 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 cherché des endroits susceptibles d'avoir des problèmes de cache. J'ai profilé et mesuré les performances en premier et à nouveau après chaque changement. Ensuite, l'entreprise s'est effondrée, mais c'était quand même un travail intéressant et instructif.

1
David Thornley

Vous obtenez de bonnes réponses ici, mais ils supposent que votre programme est presque optimal pour commencer, 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 signifie pas qu'il est presque optimal. Il faut du travail supplémentaire pour arriver à ce point.

Si je peux donner un exemple, cette réponse montre comment un programme d'aspect parfaitement raisonnable a été rendu 40 fois plus rapide par macro-optimisation . De grandes accélérations ne peuvent pas être faites dans tous les programmes tels qu'ils ont été écrits pour la première fois, mais dans beaucoup (sauf pour les très petits programmes), cela peut, selon mon expérience.

Après cela, la micro-optimisation (des points chauds) peut vous donner un bon gain.

1
Mike Dunlavey

j'utilise le compilateur Intel. sur Windows et Linux.

lorsque plus ou moins terminé, 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 computationnel 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 - peaufiner le code critique de performance. quant au reste - priorité à être correcte et maintenable - fonctions courtes, code clair compréhensible 1 an plus tard.

1
jf.

Une optimisation que j'ai utilisée en C++ crée un constructeur qui ne fait rien. Il faut appeler manuellement un init () afin de mettre l'objet dans un état de travail.

Cela présente des avantages dans le cas où j'ai besoin d'un grand vecteur de ces classes.

J'appelle reserve () pour allouer l'espace au 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é de l'espace d'adressage, mais je n'ai pas réellement consommé beaucoup de mémoire physique. J'évite les défauts de page associés aux coûts de construction associés.

Comme je génère des objets pour remplir le vecteur, je les place en utilisant init (). Cela limite le nombre total de défauts de ma page et évite d'avoir à redimensionner () le vecteur lors du remplissage.

1
EvilTeach

Pour les performances, concentrez-vous d'abord sur l'écriture de code maintenable - composant, couplé de manière lâche, etc.

L'optimiseur améliorera légèrement les performances de votre programme.

1
Ariel

Je soupçonne depuis longtemps, mais je n'ai jamais prouvé que déclarer des tableaux pour qu'ils détiennent une puissance de 2, comme le nombre d'éléments, permet à l'optimiseur de faire un réduction de la force en remplaçant une multiplication par un décalage par un nombre de bits, lors de la recherche d'éléments individuels.

0
EvilTeach

L'une des choses dont je me souviens vaguement de cobol dans les années 80, était qu'il y avait des options de l'éditeur de liens qui vous permettaient d'effectuer l'ordre dans lequel les fonctions étaient liées entre elles. Cela vous a permis d'augmenter (éventuellement) la localité du code.

Dans le même esprit. Si vous vous êtes demandé si une optimisation possible pourrait être réalisée en utilisant le modèle

for (some silly loop)
if (something)
    if (somthing else)
        if (somthing else)
            if (somthing else)
                /* This is the normal expected case */ 
            else error 4
        else error 3
    else error 2
else error 1

La tête for et les ifs peuvent s'insérer dans un bloc de cache, ce qui pourrait en théorie conduire à une exécution de boucle plus rapide.

Je suppose que les elses étant similaires pourraient être optimisés dans une certaine mesure.

Commentaires? Est-ce que je suis en train de rêver?

0
EvilTeach

Mettez des fonctions petites et/ou fréquemment appelées en haut du fichier source. Cela permet au compilateur de trouver plus facilement des opportunités d'inline.

0
Mark Ransom