web-dev-qa-db-fra.com

Quand les coûts des appels de fonction sont-ils encore importants dans les compilateurs modernes?

Je suis une personne religieuse et je fais des efforts pour ne pas commettre de péchés. C'est pourquoi j'ai tendance à écrire des fonctions petites ( plus petites que , pour reformuler Robert C. Martin) pour se conformer aux différents commandements ordonnés par le - Clean Code bible. Mais en vérifiant quelques trucs, j'ai atterri sur ce post , en dessous duquel j'ai lu ce commentaire:

N'oubliez pas que le coût d'un appel de méthode peut être important, selon la langue. Il y a presque toujours un compromis entre l'écriture de code lisible et l'écriture de code performant.

Dans quelles conditions cette déclaration citée est-elle toujours valable de nos jours étant donné la riche industrie des compilateurs modernes performants?

C'est ma seule question. Et il ne s'agit pas de savoir si je dois écrire des fonctions longues ou petites. Je souligne simplement que vos commentaires peuvent - ou non - contribuer à modifier mon attitude et me laisser incapable de résister à la tentation de blasphémateurs .

98
Billal Begueradj

Cela dépend de votre domaine.

Si vous écrivez du code pour un microcontrôleur basse consommation, le coût de l'appel de méthode peut être important. Mais si vous créez un site Web ou une application normale, le coût de l'appel de méthode sera négligeable par rapport au reste du code. Dans ce cas, il vaudra toujours plus la peine de se concentrer sur les bons algorithmes et structures de données au lieu de micro-optimisations comme les appels de méthode.

Et il est également question de compiler les méthodes pour vous. La plupart des compilateurs sont suffisamment intelligents pour intégrer des fonctions là où cela est possible.

Et enfin, il y a la règle d'or de la performance: TOUJOURS LE PROFIL D'ABORD. N'écrivez pas de code "optimisé" basé sur des hypothèses. Si vous êtes inhabituel, écrivez les deux cas et voyez ce qui est le mieux.

148
Euphoric

La surcharge des appels de fonction dépend entièrement de la langue et du niveau d'optimisation.

À un niveau ultra bas, les appels de fonction et plus encore les appels de méthode virtuelle peuvent être coûteux s'ils entraînent une mauvaise prédiction de branche ou des échecs de cache CPU. Si vous avez écrit assembleur , vous saurez également que vous avez besoin de quelques instructions supplémentaires pour enregistrer et restaurer les registres autour d'un appel. Il n'est pas vrai qu'un compilateur "suffisamment intelligent" serait en mesure d'intégrer les fonctions correctes pour éviter ce surcoût, car les compilateurs sont limités par la sémantique du langage (en particulier autour de fonctionnalités comme l'envoi de méthode d'interface ou les bibliothèques chargées dynamiquement).

À un niveau élevé, des langages comme Perl, Python, Ruby font beaucoup de tenue de livres par appel de fonction, ce qui les rend relativement coûteux. Cela est aggravé par la méta-programmation. J'ai accéléré une fois a = Python logiciel 3x juste en levant les appels de fonction hors d'une boucle très chaude. Dans le code critique pour les performances, les fonctions d'aide en ligne peuvent avoir un effet notable.

Mais la grande majorité des logiciels n'est pas si critique pour les performances que vous pourriez remarquer la surcharge des appels de fonction. Dans tous les cas, l'écriture d'un code propre et simple est payante:

  • Si votre code n'est pas critique en termes de performances, cela facilite la maintenance. Même dans les logiciels critiques pour les performances, la majorité du code ne sera pas un "point chaud".

  • Si votre code est critique en termes de performances, un code simple facilite sa compréhension et identifie les opportunités d'optimisation. Les gains les plus importants ne proviennent généralement pas de micro-optimisations telles que les fonctions intégrées, mais d'améliorations algorithmiques. Ou formulé différemment: ne faites pas la même chose plus rapidement. Trouvez un moyen d'en faire moins.

Notez que "code simple" ne signifie pas "pris en compte dans mille petites fonctions". Chaque fonction introduit également un peu de surcharge cognitive - il est plus difficile de raison à propos d'un code plus abstrait. À un moment donné, ces minuscules fonctions pourraient faire si peu que leur non-utilisation simplifierait votre code.

58
amon

Presque tous les adages sur le code de réglage pour les performances sont des cas particuliers de loi d'Amdahl . La courte déclaration humoristique de la loi d'Amdahl est

Si une partie de votre programme prend 5% du temps d'exécution et que vous optimisez cette partie pour qu'elle prenne maintenant zéro pour cent du temps d'exécution, le programme dans son ensemble ne sera que 5% plus rapide.

(Il est tout à fait possible d'optimiser les choses à zéro pour cent de l'exécution: lorsque vous vous asseyez pour optimiser un programme volumineux et compliqué, vous constaterez probablement qu'il consacre au moins une partie de son exécution à des choses il n'a pas du tout besoin de le faire .)

C'est pourquoi les gens disent normalement de ne pas s'inquiéter des coûts des appels de fonction: peu importe leur coût, normalement le programme dans son ensemble ne dépense qu'un tout petit fraction de son temps d'exécution sur les frais généraux d'appel, donc les accélérer n'aide pas beaucoup.

Mais, s'il y a une astuce que vous pouvez tirer qui rend tout la fonction appelle plus rapidement, cette astuce en vaut probablement la peine. Les développeurs de compilateurs passent beaucoup de temps à optimiser les "prologues" et "épilogues" des fonctions, car cela profite à tous les programmes compilés avec ce compilateur, même si ce n'est que un tout petit peu pour chacun.

Et, si vous avez des raisons de croire qu'un programme passe une grande partie de son exécution à des appels de fonction, alors vous devriez commencer à vous demander si certains ces appels de fonction ne sont pas nécessaires. Voici quelques règles générales pour savoir quand vous devez le faire:

  • Si le temps d'exécution par invocation d'une fonction est inférieur à une milliseconde, mais que cette fonction est appelée des centaines de milliers de fois, elle doit probablement être intégrée.

  • Si un profil du programme affiche des milliers de fonctions, et aucune d'entre elles ne prend plus de 0,1% environ de l'exécution, la surcharge des appels de fonction est probablement significatif dans l'ensemble.

  • Si vous avez " code lasagne ", dans lequel il existe de nombreuses couches d'abstraction qui ne fonctionnent pratiquement pas au-delà de la répartition vers la couche suivante, et toutes ces couches sont implémentées avec des appels de méthode virtuelle, alors il y a un il y a de fortes chances que le CPU perde beaucoup de temps sur les décrochages de pipeline de branche indirecte. Malheureusement, le seul remède pour cela est de se débarrasser de certaines couches, ce qui est souvent très difficile.

20
zwol

Je contesterai cette citation:

Il y a presque toujours un compromis entre l'écriture de code lisible et l'écriture de code performant.

Il s'agit d'une déclaration vraiment trompeuse et d'une attitude potentiellement dangereuse. Il y a des cas spécifiques où vous devez faire un compromis, mais en général, les deux facteurs sont indépendants.

Un exemple de compromis nécessaire est lorsque vous avez un algorithme simple par rapport à un algorithme plus complexe mais plus performant. Une implémentation de table de hachage est clairement plus complexe qu'une implémentation de liste chaînée, mais la recherche sera plus lente, vous devrez donc peut-être échanger la simplicité (qui est un facteur de lisibilité) pour les performances.

En ce qui concerne la surcharge des appels de fonction, la transformation d'un algorithme récursif en un itératif pourrait avoir un avantage significatif selon l'algorithme et le langage. Mais il s'agit là encore d'un scénario très spécifique, et en général les frais généraux des appels de fonction seront négligeables ou optimisés.

(Certains langages dynamiques comme Python ont un surdébit d'appel de méthode significatif. Mais si les performances deviennent un problème, vous ne devriez probablement pas utiliser Python dans le premier endroit.)

La plupart des principes de code lisible - un formatage cohérent, des noms d'identificateurs significatifs, des commentaires appropriés et utiles, etc. n'ont aucun effet sur les performances. Et certains - comme utiliser des énumérations plutôt que des chaînes - présentent également des avantages en termes de performances.

17
JacquesB

En supposant que les performances importent pour votre programme, et qu'il y a en effet beaucoup, beaucoup d'appels, le coût peut toujours ou non dépendre du type d'appel qu'il s'agit.

Si la fonction appelée est petite et que le compilateur est capable de l'intégrer, le coût sera essentiellement nul. Les compilateurs/implémentations de langage modernes ont JIT, des optimisations de temps de liaison et/ou des systèmes de modules conçus pour maximiser la capacité à intégrer des fonctions quand cela est avantageux.

OTOH, il y a un coût non évident pour les appels de fonction: leur simple existence peut inhiber les optimisations du compilateur avant et après l'appel.

Si le compilateur ne peut pas raisonner sur ce que fait la fonction appelée (par exemple, c'est une répartition virtuelle/dynamique ou une fonction dans une bibliothèque dynamique), il peut alors supposer de façon pessimiste que la fonction pourrait avoir un effet secondaire - lever une exception, modifier état global, ou modifiez toute mémoire vue par des pointeurs. Le compilateur devra peut-être enregistrer des valeurs temporaires pour sauvegarder la mémoire et les relire après l'appel. Il ne pourra pas réorganiser les instructions autour de l'appel, il peut donc ne pas être en mesure de vectoriser les boucles ou de retirer le calcul redondant des boucles.

Par exemple, si vous appelez inutilement une fonction dans chaque itération de boucle:

for(int i=0; i < /* gasp! */ strlen(s); i++) x ^= s[i];

Le compilateur peut savoir que c'est une fonction pure et la sortir de la boucle (dans un cas terrible comme cet exemple, même l'algorithme O (n ^ 2) accidentel fixe O (n)):

for(int i=0, end=strlen(s); i < end; i++) x ^= s[i];

Et puis peut-être même réécrire la boucle pour traiter les éléments 4/8/16 à la fois en utilisant des instructions larges/SIMD.

Mais si vous ajoutez un appel à du code opaque dans la boucle, même si l'appel ne fait rien et est très bon marché lui-même, le compilateur doit supposer le pire - que l'appel accède à une variable globale qui pointe vers la même mémoire que s change son contenu (même si c'est const dans votre fonction, il peut être non - const ailleurs), ce qui rend l'optimisation impossible:

for(int i=0; i < strlen(s); i++) {
    x ^= s[i];
    do_nothing();
}
5
Kornel

La surcharge de l'appel de fonction n'a pas d'importance dans la plupart des cas.

Cependant, le plus gros gain du code en ligne est optimisation du nouveau code après ligne.

Par exemple, si vous appelez une fonction avec un argument constant, l'optimiseur peut désormais replier cet argument là où il ne pouvait pas avant d'inliner l'appel. Si l'argument est un pointeur de fonction (ou lambda), l'optimiseur peut désormais également incorporer les appels à ce lambda.

C'est une grande raison pour laquelle les fonctions virtuelles et les pointeurs de fonction ne sont pas attrayants car vous ne pouvez pas les aligner du tout à moins que le pointeur de fonction réel ait été constamment plié jusqu'au site d'appel.

5
ratchet freak
  • En C++, méfiez-vous de la conception d'appels de fonction qui copient des arguments, la valeur par défaut est "passer par valeur". La surcharge des appels de fonction due à la sauvegarde des registres et d'autres éléments liés aux trames de pile peut être dépassée par une copie involontaire (et potentiellement très coûteuse) d'un objet.

  • Il y a des optimisations liées au cadre de pile que vous devez étudier avant d'abandonner le code hautement factorisé.

  • La plupart du temps, lorsque j'ai dû faire face à un programme lent, j'ai trouvé que les modifications algorithmiques entraînaient des accélérations bien plus importantes que les appels de fonction en ligne. Par exemple: un autre ingénieur a refait un analyseur qui remplissait une structure de carte de cartes. Dans le cadre de cela, il a supprimé un index mis en cache d'une carte à une carte associée logiquement. C'était un bon coup de robustesse du code, mais cela a rendu le programme inutilisable en raison d'un facteur de ralentissement de 100 en raison d'une recherche de hachage pour tous les futurs accès par rapport à l'utilisation de l'index stocké. Le profilage a montré que la plupart du temps était consacré à la fonction de hachage.

3
user2543191

Ce vieux papier pourrait répondre à votre question:

Guy Lewis Steele, Jr .. "Démystifier le mythe de 'l'appel de procédure coûteux', ou, les implémentations d'appel de procédure considérées comme nuisibles, ou, Lambda: The Ultimate GOTO". MIT AI Lab. AI Lab Memo AIM-443. Octobre 1977.

Abstrait:

Le folklore déclare que les instructions GOTO sont "bon marché", tandis que les appels de procédure sont "chers". Ce mythe est en grande partie le résultat d'implémentations de langage mal conçues. La croissance historique de ce mythe est considérée. Des idées théoriques et une implémentation existante sont discutées qui démystifient ce mythe. Il est démontré que l'utilisation sans restriction des appels de procédure permet une grande liberté de style. En particulier, tout organigramme peut être écrit comme un programme "structuré" sans introduire de variables supplémentaires. La difficulté avec l'instruction GOTO et l'appel de procédure est caractérisée comme un conflit entre les concepts de programmation abstraits et les constructions de langage concrètes.

3
Alex Vong

Oui, une prédiction de branche manquée coûte plus cher sur du matériel moderne qu'il y a des décennies, mais les compilateurs sont devenus beaucoup plus intelligents pour l'optimiser.

À titre d'exemple, considérons Java. À première vue, la surcharge des appels de fonction devrait être particulièrement dominante dans ce langage:

  • de minuscules fonctions sont répandues en raison de la convention JavaBean
  • les fonctions par défaut sont virtuelles et sont généralement
  • l'unité de compilation est la classe; le runtime prend en charge le chargement de nouvelles classes à tout moment, y compris les sous-classes qui remplacent les méthodes précédemment monomorphes

Horrifié par ces pratiques, le programmeur C moyen prédirait que Java doit être au moins un ordre de grandeur plus lent que C. Et il y a 20 ans, il aurait eu raison. Les repères modernes placent cependant idiomatique = Java code à quelques pourcents du code C équivalent. Comment est-ce possible?

L'une des raisons est que les fonctions JVM modernes en ligne appellent naturellement. Il le fait en utilisant l'incrustation spéculative:

  1. Le code fraîchement chargé s'exécute sans optimisation. Au cours de cette étape, pour chaque site d'appel, la machine virtuelle Java conserve une trace des méthodes qui ont été réellement appelées.
  2. Une fois que le code a été identifié comme point chaud de performance, le runtime utilise ces statistiques pour identifier le chemin d'exécution le plus probable et l'inline, en le préfixant avec une branche conditionnelle au cas où l'optimisation spéculative ne s'applique pas.

Autrement dit, le code:

int x = point.getX();

est réécrit en

if (point.class != Point) GOTO interpreter;
x = point.x;

Et bien sûr, le runtime est suffisamment intelligent pour remonter cette vérification de type tant que le point n'est pas attribué, ou l'élider si le type est connu du code appelant.

En résumé, si même Java gère l'inlining de méthode automatique, il n'y a aucune raison inhérente pour qu'un compilateur ne prenne pas en charge l'inlining automatique, et toutes les raisons de le faire, car l'inlining est très bénéfique pour les processeurs modernes. Je peux donc difficilement imaginer un compilateur grand public moderne ignorant cette stratégie d'optimisation la plus élémentaire, et je suppose un compilateur capable de cela, sauf preuve contraire.

2
meriton

Comme d'autres le disent, vous devez d'abord mesurer les performances de votre programme et vous ne constaterez probablement aucune différence dans la pratique.

Pourtant, d'un point de vue conceptuel, j'ai pensé éclaircir certaines choses qui se confondent dans votre question. Tout d'abord, vous demandez:

Les coûts des appels de fonction comptent-ils toujours dans les compilateurs modernes?

Remarquez les mots clés "fonction" et "compilateurs". Votre devis est subtilement différent:

N'oubliez pas que le coût d'un appel de méthode peut être important, selon la langue.

Il s'agit de méthodes, dans le sens orienté objet.

Alors que "fonction" et "méthode" sont souvent utilisées de manière interchangeable, il existe des différences en ce qui concerne leur coût (que vous demandez) et en matière de compilation (qui est le contexte que vous avez donné).

En particulier, nous devons connaître répartition statique vs répartition dynamique. Je vais ignorer les optimisations pour le moment.

Dans un langage comme C, nous appelons généralement fonctions avec répartition statique. Par exemple:

int foo(int x) {
  return x + 1;
}

int bar(int y) {
  return foo(y);
}

int main() {
  return bar(42);
}

Lorsque le compilateur voit l'appel foo(y), il sait à quelle fonction ce nom foo fait référence, de sorte que le programme de sortie peut passer directement à la fonction foo, ce qui est assez pas cher. C'est ce que répartition statique signifie.

L'alternative est répartition dynamique, où le compilateur ne sait pas sait quelle fonction est appelée. À titre d'exemple, voici du code Haskell (car l'équivalent C serait compliqué!):

foo x = x + 1

bar f x = f x

main = print (bar foo 42)

Ici, la fonction bar appelle son argument f, qui pourrait être n'importe quoi. Par conséquent, le compilateur ne peut pas simplement compiler bar en une instruction de saut rapide, car il ne sait pas où aller. Au lieu de cela, le code que nous générons pour bar va déréférencer f pour trouver la fonction vers laquelle il pointe, puis y accéder. C'est ce que répartition dynamique signifie.

Ces deux exemples sont pour fonctions. Vous avez mentionné méthodes, qui peut être considéré comme un style particulier de fonction distribuée dynamiquement. Par exemple, voici du Python:

class A:
  def __init__(self, x):
    self.x = x

  def foo(self):
    return self.x + 1

def bar(y):
  return y.foo()

z = A(42)
bar(z)

L'appel y.foo() utilise la répartition dynamique, car il recherche la valeur de la propriété foo dans l'objet y et appelle tout ce qu'il trouve; il ne sait pas que y aura la classe A, ou que la classe A contient une méthode foo, donc nous ne pouvons pas simplement sauter directement à elle.

OK, c'est l'idée de base. Notez que la répartition statique est plus rapide que la répartition dynamique peu importe que nous compilions ou interprétions; toutes choses étant égales par ailleurs. Le déréférencement entraîne un coût supplémentaire dans les deux cas.

Comment cela affecte-t-il les compilateurs modernes et optimisants?

La première chose à noter est que la répartition statique peut être optimisée plus fortement: lorsque nous savons à quelle fonction nous allons, nous pouvons faire des choses comme l'inline. Avec la répartition dynamique, nous ne savons pas que nous sautons jusqu'à l'exécution, donc nous ne pouvons pas faire beaucoup d'optimisation.

Deuxièmement, il est possible dans certaines langues de inférer où certaines répartitions dynamiques finiront par sauter, et donc de les optimiser en répartition statique. Cela nous permet d'effectuer d'autres optimisations comme l'inline, etc.

Dans l'exemple ci-dessus Python une telle inférence est assez désespérée, car Python permet à d'autres codes de remplacer les classes et les propriétés, il est donc difficile d'inférer beaucoup de ce qui tiendra tous les cas.

Si notre langage nous permet d'imposer plus de restrictions, par exemple en limitant y à la classe A à l'aide d'une annotation, alors nous pourrions utiliser ces informations pour déduire la fonction cible. Dans les langages avec sous-classement (qui sont presque tous les langages avec classes!), Cela ne suffit pas, car y peut en fait avoir une (sous) classe différente, nous aurions donc besoin d'informations supplémentaires comme Java final annotations pour savoir exactement quelle fonction sera appelée.

Haskell n'est pas un langage OO, mais nous pouvons déduire la valeur de f en insérant bar (qui est statiquement distribué ) dans main, en remplaçant foo par y. Étant donné que la cible de foo dans main est connue de manière statique, l'appel est distribué de façon statique , et sera probablement complètement intégré et optimisé (car ces fonctions sont petites, le compilateur est plus susceptible de les intégrer; bien que nous ne puissions pas compter sur cela en général).

Par conséquent, le coût se résume à:

  • La langue envoie-t-elle votre appel de manière statique ou dynamique?
  • Si c'est le dernier, le langage permet-il à l'implémentation de déduire la cible en utilisant d'autres informations (par exemple, types, classes, annotations, inscriptions, etc.)?
  • Dans quelle mesure la répartition statique (présumée ou non) peut-elle être optimisée?

Si vous utilisez un langage "très dynamique", avec beaucoup de répartition dynamique et peu de garanties disponibles pour le compilateur, chaque appel entraînera un coût. Si vous utilisez un langage "très statique", un compilateur mature produira du code très rapide. Si vous êtes entre les deux, cela peut dépendre de votre style de codage et de la qualité de l'implémentation.

2
Warbo

N'oubliez pas que le coût d'un appel de méthode peut être important, selon la langue. Il y a presque toujours un compromis entre l'écriture de code lisible et l'écriture de code performant.

Cela dépend, malheureusement, fortement de:

  • la chaîne d'outils du compilateur, y compris le JIT le cas échéant,
  • le domaine.

Tout d'abord, la première loi d'optimisation des performances est le profil en premier . Il existe de nombreux domaines où les performances de la partie logicielle sont non pertinentes aux performances de l'ensemble de la pile: appels de base de données, opérations réseau, opérations OS, ...

Cela signifie que les performances du logiciel sont complètement hors de propos, même si elles n'améliorent pas la latence, l'optimisation du logiciel peut entraîner des économies d'énergie et des économies de matériel (ou des économies de batterie pour les applications mobiles), ce qui peut avoir de l'importance.

Cependant, ceux-ci ne peuvent généralement PAS être attirés par les yeux, et souvent les améliorations algorithmiques l'emportent largement sur les micro-optimisations.

Donc, avant d'optimiser, vous devez comprendre ce pour quoi vous optimisez ... et si cela en vaut la peine.


Maintenant, en ce qui concerne les performances logicielles pures, elles varient considérablement entre les chaînes d'outils.

Un appel de fonction comporte deux coûts:

  • le coût d'exécution,
  • le coût du temps de compilation.

Le coût d'exécution est assez évident; pour exécuter un appel de fonction, une certaine quantité de travail est nécessaire. En utilisant C sur x86 par exemple, un appel de fonction nécessitera (1) de répandre des registres dans la pile, (2) de pousser des arguments vers les registres, d'effectuer l'appel, puis (3) de restaurer les registres à partir de la pile. Voir ce résumé des conventions d'appel pour voir le travail impliqué .

Ce déversement/restauration de registres prend un temps non négligeable (des dizaines de cycles CPU).

On s'attend généralement à ce que ce coût soit trivial par rapport au coût réel de l'exécution de la fonction, cependant certains modèles sont contre-productifs ici: getters, fonctions gardées par une condition simple, etc ...

En dehors de interprètes, un programmeur espère donc que son compilateur ou [~ # ~] jit [~ # ~] = optimisera les appels de fonction inutiles; bien que cet espoir puisse parfois ne pas porter ses fruits. Parce que les optimiseurs ne sont pas magiques.

Un optimiseur peut détecter qu'un appel de fonction est trivial, et en ligne l'appel: essentiellement, copier/coller le corps de la fonction sur le site de l'appel. Ce n'est pas toujours une bonne optimisation (peut induire un gonflement) mais en général cela vaut la peine car l'inline expose le contexte, et le contexte permet plus d'optimisations.

Un exemple typique est:

void func(condition: boolean) {
    if (condition) {
        doLotsOfWork();
    }
}

void call() { func(false); }

Si func est en ligne, l'optimiseur se rendra compte que la branche n'est jamais prise et optimisera call en void call() {}.

En ce sens, les appels de fonction, en masquant les informations de l'optimiseur (s'ils ne sont pas encore intégrés), peuvent inhiber certaines optimisations. Les appels de fonction virtuelle en sont particulièrement coupables, car la dévirtualisation (prouver quelle fonction est finalement appelée au moment de l'exécution) n'est pas toujours facile.


En conclusion, mon conseil est d'écrire clairement en premier, en évitant la pessimisation algorithmique prématurée (complexité cubique ou pire piqûres rapidement), puis en optimisant uniquement ce qui doit être optimisé.

2
Matthieu M.

"N'oubliez pas que le coût d'un appel de méthode peut être important, selon la langue. Il y a presque toujours un compromis entre l'écriture de code lisible et l'écriture de code performant."

Dans quelles conditions cette déclaration citée est-elle toujours valable de nos jours compte tenu de la richesse de l'industrie des compilateurs modernes performants?

Je vais juste dire carrément jamais. Je pense que la citation est imprudente de la jeter.

Bien sûr, je ne dis pas la vérité complète, mais je ne me soucie pas tellement d'être véridique. C'est comme dans ce film Matrix, j'ai oublié si c'était 1 ou 2 ou 3 - je pense que c'était celui avec l'actrice italienne sexy avec les gros melons (je n'en aimais pas vraiment mais le premier), quand le Une dame d'Oracle a dit à Keanu Reeves, "Je viens de vous dire ce que vous aviez besoin d'entendre," ou quelque chose dans ce sens, c'est ce que je veux faire maintenant.

Les programmeurs n'ont pas besoin d'entendre cela. S'ils sont expérimentés avec les profileurs dans leur main et que la citation est quelque peu applicable à leurs compilateurs, ils le sauront déjà et l'apprendront de la bonne manière à condition de comprendre leur sortie de profilage et pourquoi certains appels de feuilles sont des points chauds, grâce à la mesure. S'ils ne sont pas expérimentés et n'ont jamais profilé leur code, c'est la dernière chose qu'ils doivent entendre, qu'ils devraient commencer à compromettre superstitieusement la façon dont ils écrivent le code au point de tout inclure avant même d'identifier les points chauds dans l'espoir que cela va devenir plus performant.

Quoi qu'il en soit, pour une réponse plus précise, cela dépend. Une partie de la cargaison de conditions est déjà répertoriée parmi les bonnes réponses. Les conditions possibles en choisissant simplement une langue sont déjà énormes elles-mêmes, comme C++ qui devrait entrer dans la répartition dynamique dans les appels virtuels et quand il peut être optimisé et sous quels compilateurs et même les éditeurs de liens, et cela justifie déjà une réponse détaillée et encore moins d'essayer pour faire face aux conditions dans toutes les langues et compilateurs possibles. Mais je vais ajouter par-dessus, "qui s'en soucie?" parce que même en travaillant dans des domaines critiques pour les performances comme le lancer de rayons, la dernière chose que je commencerai à faire à l'avance, ce sont les méthodes d'alignement manuel avant d'avoir toutes les mesures.

Je crois que certaines personnes deviennent trop zélées à l'idée de ne jamais faire de micro-optimisations avant de mesurer. Si l'optimisation pour la localité de référence compte comme une micro-optimisation, alors je commence souvent à appliquer ces optimisations dès le début avec un état d'esprit de conception orienté données dans des domaines dont je sais avec certitude qu'ils seront critiques en termes de performances (code de raytracing, par exemple), car sinon je sais que je devrai réécrire de grandes sections peu de temps après avoir travaillé dans ces domaines pendant des années. L'optimisation de la représentation des données pour les accès au cache peut souvent avoir le même type d'amélioration des performances que les améliorations algorithmiques, sauf si nous parlons de temps quadratique à linéaire.

Mais je ne vois jamais, jamais une bonne raison de commencer l'inlining avant les mesures, d'autant plus que les profileurs sont décents pour révéler ce qui pourrait bénéficier de l'inlining, mais pas pour révéler ce qui pourrait bénéficier de ne pas être inline (et ne pas l'inliner peut réellement accélérer le code si le L'appel de fonction non doublé est un cas rare, améliorant la localité de référence de l'icache pour le code chaud et parfois même permettant aux optimiseurs de faire un meilleur travail pour le chemin d'exécution du cas commun).

1
user204677