Je suis une personne religieuse et fais des efforts pour ne pas commettre de péchés. C'est pourquoi j'ai tendance à écrire de petites fonctions ( plus petites , pour reformuler Robert C. Martin) afin de se conformer aux différents commandements ordonnés par la bible de Clean Code . Mais en vérifiant certaines choses, 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, en fonction de la langue. Il y a presque toujours un compromis entre écrire du code lisible et écrire du code performant.
Sous quelles conditions cette déclaration citée est-elle toujours valable de nos jours compte tenu de la riche industrie des compilateurs modernes performants?
C'est ma seule question. Et il ne s'agit pas de savoir si je devrais écrire de longues ou de petites fonctions. Je souligne simplement que vos réactions peuvent (ou non) contribuer à modifier mon attitude et me laisser incapable de résister à la tentation des blasphémateurs .
la source
for(Integer index = 0, size = someList.size(); index < size; index++)
au lieu de simplementfor(Integer index = 0; index < someList.size(); index++)
. Le fait que votre compilateur ait été réalisé au cours des dernières années ne signifie pas nécessairement que vous pouvez renoncer au profilage.main()
, d'autres divisent le tout en une cinquantaine de fonctions minuscules et sont totalement illisibles. L'astuce consiste, comme toujours, à trouver un bon équilibre .Réponses:
Cela dépend de votre domaine.
Si vous écrivez du code pour un microcontrôleur à faible consommation, le coût d’appel de méthode peut être important. Mais si vous créez un site Web ou une application normale, le coût des appels de méthode sera négligeable par rapport au reste du code. Dans ce cas, il sera toujours plus intéressant de se concentrer sur les algorithmes et les structures de données appropriés plutôt que sur les micro-optimisations telles que les appels de méthodes.
Et il est également question de compiler en ligne les méthodes pour vous. La plupart des compilateurs sont suffisamment intelligents pour intégrer des fonctions dans la mesure du possible.
Et enfin, il y a la règle d'or de la performance: TOUJOURS PROFIL AVANT. N'écrivez pas de code "optimisé" basé sur des hypothèses. Si vous êtes inutilisé, écrivez les deux cas et voyez lequel est le meilleur.
la source
La surcharge des appels de fonction dépend entièrement de la langue et du niveau que vous optimisez.
À un niveau très bas, les appels de fonction et encore plus les appels de méthodes virtuelles peuvent être coûteux s'ils conduisent à des erreurs de prédiction de branche ou à des erreurs de cache du processeur. Si vous avez écrit assembler , 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 capable d’aligner les fonctions correctes pour éviter cette surcharge, car les compilateurs sont limités par la sémantique du langage (en particulier autour de fonctionnalités telles que la distribution de méthode d’interface ou les bibliothèques chargées dynamiquement).
À un niveau élevé, des langages comme Perl, Python, Ruby font beaucoup de comptabilité par appel de fonction, ce qui rend ceux-ci relativement coûteux. Ceci est aggravé par la méta-programmation. Une fois, j’ai accéléré un logiciel 3x Python simplement en soulevant des appels de fonction d’une boucle très chaude. Dans les codes critiques en termes de performances, les fonctions d'assistance en ligne peuvent avoir un effet notable.
Mais la grande majorité des logiciels n’est pas aussi critique en termes de performances que vous seriez en mesure de remarquer le temps système d’appel de fonction. Dans tous les cas, écrire du code propre et simple rapporte:
Si votre code n'est pas critique en termes de performances, cela facilite la maintenance. Même dans les logiciels critiques en termes de performances, la majorité du code ne sera pas un «point chaud».
Si votre code est critique en termes de performances, un code simple facilite la compréhension du code et la détection des opportunités d'optimisation. Les plus gros gains ne proviennent généralement pas de micro-optimisations telles que des fonctions en ligne, mais d'améliorations algorithmiques. Ou exprimé différemment: ne faites pas la même chose plus rapidement. Trouvez un moyen de faire moins.
Notez que «code simple» ne signifie pas «intégré dans mille fonctions minuscules». Chaque fonction introduit également un peu de surcharge cognitive - il est plus difficile de raisonner sur un code plus abstrait. À un moment donné, ces minuscules fonctions pourraient faire si peu que ne pas les utiliser simplifierait votre code.
la source
Presque tous les adages sur le code de réglage pour l'exécution sont des cas particuliers de la loi d' Amdahl . La déclaration courte et humoristique de la loi d'Amdahl est
(Il est tout à fait possible d’optimiser jusqu’à zéro pour cent du temps d’exécution: lorsque vous optimisez un programme volumineux et compliqué, vous avez toutes les chances de penser qu’il passe au moins une partie de son exécution à des tâches qu’il n’a pas du tout besoin de faire. .)
C'est pourquoi les gens disent normalement qu'ils ne s'inquiètent pas du coût des appels de fonction: peu importe leur coût, normalement, le programme dans son ensemble ne dépense qu'une infime fraction de son temps d'exécution en temps système, de sorte que son accélération n'aide pas beaucoup. .
Mais si vous pouvez tirer un truc qui accélère tous les appels de fonction, ce truc en vaut probablement la peine. Les développeurs de compilateurs passent beaucoup de temps à optimiser les fonctions "prologues" et "épilogues", car cela profite à tous les programmes compilés avec ce compilateur, même si ce n'est qu'un tout petit peu pour chacun.
Et, si vous avez des raisons de croire qu'un programme est passé beaucoup de son exécution juste faire des appels de fonction, alors vous devriez commencer à penser à savoir si certains de ces appels de fonction ne sont pas nécessaires. Voici quelques règles de base pour savoir quand vous devez le faire:
Si l'exécution d'une fonction par invocation est inférieure à une milliseconde, mais que cette fonction est appelée des centaines de milliers de fois, elle devrait probablement être en ligne.
Si un profil du programme indique des milliers de fonctions et qu'aucune d'elles ne nécessite plus de 0,1% environ de son exécution, le temps système de traitement des appels de fonction est probablement important.
Si vous avez un " code de lasagne " dans lequel il existe de nombreuses couches d'abstraction qui ne fonctionnent pratiquement pas au-delà de l'envoi à la couche suivante, et que toutes ces couches sont implémentées avec des appels de méthodes virtuelles, il y a de fortes chances que le processeur gaspille beaucoup de temps sur les stands de pipeline indirects. Malheureusement, le seul remède à cela est de se débarrasser de certaines couches, ce qui est souvent très difficile.
la source
final
classes et des méthodes, le cas échéant, en Java, ou desvirtual
méthodes autres que les méthodes en C # ou C ++), l’indirection peut être éliminée par le compilateur / runtime et vous ' Vous verrez un gain sans restructuration massive. Comme @JorgWMittag le souligne ci-dessus, la JVM peut même s'aligner dans les cas où il n'est pas prouvable que l'optimisation soit ...Je vais contester cette citation:
C'est une déclaration vraiment trompeuse et une attitude potentiellement dangereuse. Il existe 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 opposition à 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 de la simplicité (ce qui est un facteur de lisibilité) en termes de performances.
En ce qui concerne le temps système d’appel de fonction, transformer un algorithme récursif en itératif peut présenter un avantage significatif en fonction de l’algorithme et du langage. Mais il s’agit là encore d’un scénario très spécifique et, en général, la surcharge des appels de fonction sera négligeable ou optimisée.
(Certains langages dynamiques comme Python entraînent une surcharge d’appel de méthode. Toutefois, si les performances deviennent un problème, vous ne devriez probablement pas utiliser Python en premier lieu.)
La plupart des principes de code lisible - mise en forme cohérente, noms d'identifiant significatifs, commentaires appropriés et utiles, etc., n'ont aucun effet sur les performances. Et certains, comme l'utilisation d'énums plutôt que de chaînes, présentent également des avantages en termes de performances.
la source
La surcharge de l'appel de fonction est sans importance dans la plupart des cas.
Cependant, le plus gros gain de code en ligne est l' optimisation du nouveau code après l'inline .
Par exemple, si vous appelez une fonction avec un argument constant, l'optimiseur peut maintenant plier cet argument de manière constante, comme auparavant. Si l'argument est un pointeur de fonction (ou lambda), l'optimiseur peut désormais intégrer également les appels à cette lambda.
C'est l'une des principales raisons pour lesquelles les fonctions virtuelles et les pointeurs de fonction ne sont pas attrayants, car vous ne pouvez les aligner du tout que si le pointeur de fonction réel a été constamment plié jusqu'au site d'appel.
la source
En supposant que les performances importent pour votre programme et qu’il comporte effectivement de très nombreux appels, le coût peut ou non être important, en fonction du type d’appel.
Si la fonction appelée est petite et que le compilateur est en mesure de la mettre en ligne, le coût sera essentiellement nul. Les compilateurs modernes et les implémentations de langage 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 lorsque cela est bénéfique.
OTOH, il y a un coût non évident pour les appels de fonction: leur simple existence peut empêcher les optimisations du compilateur avant et après l'appel.
Si le compilateur ne peut pas raisonner sur le rôle de la fonction appelée (par exemple, son envoi virtuel / dynamique ou une fonction dans une bibliothèque dynamique), il peut être amené à assumer avec pessimisme que la fonction peut avoir un effet secondaire quelconque: lever une exception, modifier état global, ou changer toute mémoire vue par des pointeurs. Le compilateur devra peut-être sauvegarder des valeurs temporaires dans la mémoire vive et les relire après l'appel. Il ne sera pas en mesure de réordonner les instructions autour de l'appel. Il risque donc d'être incapable de vectoriser des boucles ou de lever des calculs redondants en boucle.
Par exemple, si vous appelez inutilement une fonction à chaque itération de boucle:
Le compilateur peut savoir que c'est une fonction pure et le déplacer hors de la boucle (dans un cas aussi terrible que cet exemple, même, l'algorithme accidentel O (n ^ 2) est défini sur O (n)):
Et puis peut-être même réécrivez la boucle pour traiter 4/8/16 éléments à la fois en utilisant des instructions larges / SIMD.
Mais si vous ajoutez un appel à un code opaque dans la boucle, même si l'appel ne fait rien et qu'il est très bon marché lui-même, le compilateur doit assumer le pire: l'appel appellera une variable globale pointant vers la même mémoire que le
s
changement. son contenu (même s'il fait partie deconst
votre fonction, il peut ne pas êtreconst
ailleurs), rendant l'optimisation impossible:la source
Ce vieil article pourrait répondre à votre question:
Abstrait:
la source
Dans C ++, méfiez-vous de la conception d'appels de fonction qui copient les arguments, la valeur par défaut est "passer par la valeur". La surcharge de l'appel de fonction due aux registres de sauvegarde et à d'autres éléments liés à la pile peut être submergée par une copie non intentionnelle (et potentiellement très coûteuse) d'un objet.
Il existe des optimisations liées aux images 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 constaté que les modifications algorithmiques produisaient des accélérations bien plus rapides que les appels de fonction en ligne. Par exemple: un autre ingénieur a refait un analyseur syntaxique qui a rempli une structure de carte de cartes. Dans ce cadre, il a supprimé un index mis en cache d'une carte à un autre associé de manière logique. C’était un bon choix pour la robustesse du code, mais il a rendu le programme inutilisable en raison d’un facteur de ralentissement de 100 dû à l’exécution d’une recherche de hachage pour tous les accès futurs par rapport à l’utilisation de l’index stocké. Le profilage a montré que la majeure partie du temps était consacrée à la fonction de hachage.
la source
Oui, une prévision de branche manquée est plus coûteuse sur le matériel moderne qu'il ne l'était il y a plusieurs décennies, mais les compilateurs sont devenus beaucoup plus intelligents pour l'optimiser.
Par exemple, considérons Java. A première vue, le préfixe d'appel de fonction devrait être particulièrement dominant dans cette langue:
Horrifié par ces pratiques, le programmeur C moyen prédirait que Java doit être au moins un ordre de grandeur plus lent que le C. Il y a 20 ans, il aurait eu raison. Les benchmarks modernes placent toutefois du code Java idiomatique à quelques pour cent du code C équivalent. Comment est-ce possible?
Une des raisons est que les appels de fonctions en ligne des machines virtuelles modernes (JVM) modernes sont bien sûr de mise. Il le fait en utilisant l'inline spéculative:
C'est le code:
est réécrit pour
Et bien sûr, le moteur d’exécution est suffisamment intelligent pour passer à la vérification du type tant que le point n’est pas attribué, ou le supprimer si le type est connu du code appelant.
En résumé, si même Java gère l'inlignage automatique de méthodes, il n'y a aucune raison inhérente pour laquelle un compilateur ne peut pas prendre 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 ces stratégies d'optimisation les plus élémentaires, et présumerais qu'un compilateur en est capable, sauf preuve du contraire.
la source
Comme d’autres le disent, vous devriez d’abord mesurer la performance de votre programme et vous ne constaterez probablement aucune différence dans la pratique.
Néanmoins, d'un point de vue conceptuel, je pensais éclaircir quelques points qui sont confondus dans votre question. Tout d'abord, vous demandez:
Remarquez les mots clés "fonction" et "compilateurs". Votre citation est subtile différente:
Il s’agit de méthodes , au sens orienté objet.
Bien que "fonction" et "méthode" soient souvent utilisés de manière interchangeable, il existe des différences quant au coût (dont vous parlez) et à la compilation (qui est le contexte que vous avez donné).
Nous devons en particulier connaître la répartition statique par rapport à la répartition dynamique . Je vais ignorer les optimisations pour le moment.
Dans un langage comme C, nous appelons généralement des fonctions à dispatch statique . Par exemple:
Lorsque le compilateur voit l'appel
foo(y)
, il sait à quelle fonction sonfoo
nom fait référence, ainsi le programme de sortie peut passer directement à lafoo
fonction, ce qui est relativement peu coûteux. C'est ce que l'envoi statique signifie.L'alternative est la répartition dynamique , où le compilateur ne sait pas quelle fonction est appelée. Voici un exemple de code Haskell (car l’équivalent C serait désordonné!):
Ici, la
bar
fonction appelle son argumentf
, ce qui pourrait être n'importe quoi. Par conséquent, le compilateur ne peut pas simplement compilerbar
une instruction de saut rapide, car il ne sait pas où aller. Au lieu de cela, le code que nous générons pourbar
déréférenceraf
pour rechercher la fonction à laquelle il pointe, puis y accéder. C'est ce que l'envoi dynamique signifie.Ces deux exemples concernent des fonctions . Vous avez mentionné les méthodes , qui peuvent être considérées comme un style particulier de fonction à distribution dynamique. Par exemple, voici quelques exemples de Python:
L'
y.foo()
appel utilise la répartition dynamique, puisqu'il recherche la valeur de lafoo
propriété dans l'y
objet et appelle tout ce qu'il trouve. il ne sait pas qu'ily
aura une classeA
, ou que laA
classe contient unefoo
méthode, nous ne pouvons donc pas y aller directement.OK, c'est l'idée de base. Notez que l' envoi statique est plus rapide que l' envoi dynamique indépendamment du fait que nous compilons ou interprétons; tout le reste étant égal. Le déréférencement entraîne un coût supplémentaire dans les deux sens.
Alors, 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 lourdement: lorsque nous savons à quelle fonction nous accédons, nous pouvons faire des choses comme l’intégration en ligne. Avec la répartition dynamique, nous ne savons pas que nous sautons jusqu'au moment de l'exécution, de sorte que nous ne pouvons pas optimiser beaucoup.
Deuxièmement, dans certaines langues, il est possible de déduire où certaines dépêches dynamiques finiront par sauter, et donc de les optimiser en répartition statique. Cela nous permet d’effectuer d’autres optimisations comme l’alignement, etc.
Dans l'exemple Python ci-dessus, une telle inférence est plutôt sans espoir, car Python permet à un autre code de remplacer les classes et les propriétés. Il est donc difficile d'en déduire ce qui va se passer dans tous les cas.
Si notre langage nous permet d'imposer davantage de restrictions, par exemple en limitant la
y
classe à l'A
aide d'une annotation, nous pourrions utiliser cette information pour déduire la fonction cible. Dans les langues avec sous-classes (c'est-à-dire presque toutes les langues avec classes!), Cela ne suffit pas, car ily
peut en fait avoir une (sous) classe différente. Nous aurions donc besoin d'informations supplémentaires comme lesfinal
annotations de Java pour savoir exactement quelle fonction sera appelée.Haskell n'est pas un langage OO, mais nous pouvons déduire la valeur de
f
inliningbar
(qui est statiquement expédié) dansmain
, en remplaçantfoo
pary
. Puisque la cible defoo
inmain
est connue de manière statique, l'appel est envoyé de manière statique et sera probablement totalement aligné et optimisé (ces fonctions étant petites, le compilateur est plus susceptible de les aligner; nous ne pouvons cependant pas compter sur cela en général. ).Le coût revient donc à:
Si vous utilisez un langage "très dynamique", avec une bonne répartition dynamique et peu de garanties pour le compilateur, chaque appel engendrera un coût. Si vous utilisez un langage "très statique", un compilateur mature produira un code très rapide. Si vous êtes entre les deux, cela peut dépendre de votre style de codage et du degré d'implémentation de votre mise en œuvre.
la source
Malheureusement, cela dépend fortement de:
Tout d’abord, la première loi d’optimisation des performances est le profil d’abord . Il existe de nombreux domaines dans lesquels les performances de la partie logicielle sont sans rapport avec celles 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 cela n'améliore pas la latence, l'optimisation du logiciel peut entraîner des économies d'énergie et des économies matérielles (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 remarqués, et souvent les améliorations algorithmiques l'emportent largement sur les micro-optimisations.
Avant d’optimiser, vous devez donc comprendre pourquoi vous optimisez ... et si cela en vaut la peine.
Maintenant, en ce qui concerne les performances logicielles pures, elles varient énormément d'une chaîne à une autre.
Un appel de fonction entraîne deux coûts:
Le coût d'exécution est plutôt évident. pour effectuer 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 renverser des registres dans la pile, (2) de pousser des arguments dans les registres, d'effectuer l'appel et, par la suite (3) de restaurer les registres à partir de la pile. Voir ce résumé des conventions d’appel pour voir le travail impliqué .
Ce registre déversé / restauré prend un nombre de fois non négligeable (des dizaines de cycles de processeur).
On s’attend généralement à ce que ce coût soit trivial par rapport au coût réel d’exécution de la fonction, mais certains modèles sont contre-productifs ici: accesseurs, fonctions gardées par une condition simple, etc.
Outre les interprètes , un programmeur espère donc que son compilateur ou JIT optimisera les appels de fonctions 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 : il s'agit essentiellement de 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 en vaut la peine, car l’inline expose le contexte et le contexte permet davantage d’optimisations.
Un exemple typique est:
Si
func
est inline, l'optimiseur réalisera que la branche est jamais prise, et d' optimisercall
àvoid call() {}
.En ce sens, les appels de fonction, en masquant les informations de l'optimiseur (s'ils ne sont pas encore en ligne), peuvent inhiber certaines optimisations. Les appels de fonctions virtuelles en sont particulièrement responsables, car la dévirtualisation (prouver quelle fonction est appelée en dernier lieu au moment de l'exécution) n'est pas toujours facile.
En conclusion, mon conseil est d'écrire d' abord clairement , en évitant une pessimisation algorithmique prématurée (complexité cubique ou pire morsures rapidement), puis d'optimiser uniquement ce qui doit être optimisé.
la source
Je vais juste dire carrément jamais. Je crois que la citation est imprudente à jeter simplement là-bas.
Bien sûr, je ne dis pas la vérité complète, mais je ne me soucie pas d’être aussi véridique. C’est comme dans Matrix, j’avais oublié si c’était 1, 2 ou 3 - je pense que c’est celui avec la sexy actrice italienne avec les gros melons (je n’ai vraiment aimé que le premier), quand Dame d'oracle a dit à Keanu Reeves: "Je viens de vous dire ce que vous aviez besoin d'entendre", ou quelque chose du genre, c'est ce que je veux faire maintenant.
Les programmeurs n'ont pas besoin d'entendre ça. S'ils connaissent les profileurs à la main et que la citation est un peu applicable à leurs compilateurs, ils le sauront déjà et l'apprendront de la bonne manière à condition de bien comprendre la sortie de leur profilage et pourquoi certains appels de feuille sont des points chauds, grâce à la mesure. S'ils ne sont pas expérimentés et qu'ils n'ont jamais défini leur code, c'est la dernière chose dont ils ont besoin d'entendre, à savoir qu'ils devraient commencer à compromettre superstitieusement la manière dont ils écrivent le code au point de tout aligner avant même d'identifier les points chauds dans l'espoir que cela va se produire. devenir plus performant.
Quoi qu'il en soit, pour une réponse plus précise, cela dépend. Certaines des conditions remplies par bateau figurent déjà parmi les bonnes réponses. Les conditions possibles qui consistent à choisir un seul langage sont déjà énormes, comme le C ++, qui devrait être intégré de manière dynamique dans les appels virtuels et quand il peut être optimisé et sous quels compilateurs et même éditeurs de liens, et qui justifie déjà une réponse détaillée, encore moins d'essayer s'attaquer aux conditions dans tous les langages et compilateurs possibles. Mais je vais ajouter en haut, "qui s'en soucie?" Même si je travaille dans des domaines tels que le lancer de rayons dans des domaines critiques en termes de performances, la dernière chose que je commence à faire dès le départ est la mise en place manuelle de méthodes en ligne avant de prendre des mesures.
Je crois que certaines personnes deviennent trop zélées pour suggérer que vous ne devriez jamais faire de micro-optimisation 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 que certains seront essentiels à la performance (code de traçage, par exemple), sinon, je sais que je vais devoir 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 en cache peut souvent apporter le même type d'amélioration des performances que celle des améliorations algorithmiques, à moins que nous ne parlions de temps quadratique à linéaire.
Mais je ne vois jamais une bonne raison de commencer à aligner avant les mesures, d’autant plus que les profileurs sont bien placés pour révéler ce qui pourrait bénéficier de l’inline, mais pas pour savoir ce qui pourrait bénéficier de ne pas être en ligne (et ne pas en ligne peut effectivement rendre le code plus rapidement L'appel de fonction non doublé est un cas rare, améliorant la localité de référence pour le icache pour le code dynamique et permettant même parfois aux optimiseurs de faire un meilleur travail pour le chemin d'exécution de la casse courante).
la source