Quelle est l'incidence des appels de fonction sur les performances?


13

L'extraction de fonctionnalités dans des méthodes ou des fonctions est indispensable pour la modularité, la lisibilité et l'interopérabilité du code, en particulier dans la POO.

Mais cela signifie que davantage d'appels de fonctions seront effectués.

Comment la division de notre code en méthodes ou fonctions affecte-t-elle réellement les performances dans les langages modernes * ?

* Les plus populaires: C, Java, C ++, C #, Python, JavaScript, Ruby ...



1
Je pense que chaque implémentation de langage digne de ce nom fait de l'inline depuis plusieurs décennies. IOW, les frais généraux sont précisément de 0.
Jörg W Mittag

1
"plus d'appels de fonction seront effectués" n'est souvent pas vrai car beaucoup de ces appels auront leur surcharge optimisée par les différents compilateurs / interprètes qui traitent votre code et les éléments en ligne. Si votre langue n'a pas ce genre d'optimisations, je ne la considérerai peut-être pas comme moderne.
Ixrec

2
Comment cela affectera-t-il les performances? Cela le rendra plus rapide, ou plus lent, ou ne le changera pas, selon le langage spécifique que vous utilisez et quelle est la structure du code réel et éventuellement sur quelle version du compilateur que vous utilisez et peut-être même sur quelle plate-forme vous '' re en marche. Chaque réponse que vous obtiendrez sera une variation de cette incertitude, avec plus de mots et plus de preuves à l'appui.
GrandOpener

1
L'impact, le cas échéant, est si faible que vous, une personne, ne le remarquerez jamais . Il y a d'autres choses bien plus importantes à se soucier. Comme si les tabulations doivent avoir 5 ou 7 espaces.
MetaFight

Réponses:


21

Peut être. Le compilateur pourrait décider "hé, cette fonction n'est appelée que quelques fois, et je suis censé optimiser la vitesse, donc je vais juste intégrer cette fonction". Essentiellement, le compilateur remplacera l'appel de fonction par le corps de la fonction. Par exemple, le code source ressemblerait à ceci.

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

Le compilateur décide de s'aligner DoSomethingElseet le code devient

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

Lorsque les fonctions ne sont pas intégrées, oui, il y a un impact sur les performances pour effectuer un appel de fonction. Cependant, c'est un coup si minuscule que seul le code extrêmement performant va se soucier des appels de fonction. Et sur ces types de projets, le code est généralement écrit en assembleur.

Les appels de fonction (selon la plate-forme) impliquent généralement quelques 10s d'instructions, y compris l'enregistrement / la restauration de la pile. Certains appels de fonction consistent en une instruction de saut et de retour.

Mais il y a d'autres choses qui peuvent affecter les performances des appels de fonction. La fonction appelée peut ne pas être chargée dans le cache du processeur, provoquant un échec de cache et forçant le contrôleur de mémoire à saisir la fonction de la RAM principale. Cela peut provoquer un gros coup pour les performances.

En bref: les appels de fonction peuvent ou non avoir un impact sur les performances. La seule façon de le savoir est de profiler votre code. N'essayez pas de deviner où sont les taches de code lentes, car le compilateur et le matériel ont des trucs incroyables dans leurs manches. Profilez le code pour obtenir l'emplacement des zones lentes.


1
J'ai vu avec des compilateurs modernes (gcc, clang) dans des situations où je tenais vraiment à ce qu'ils créent un code assez mauvais pour les boucles à l' intérieur d'une grande fonction . Extraire la boucle dans une fonction statique n'a pas aidé à cause de l'inline. Extraire la boucle dans une fonction externe a créé dans certains cas des améliorations de vitesse significatives (mesurables dans les benchmarks).
gnasher729

1
Je voudrais m'appuyer sur cela et dire que OP devrait faire attention à l' optimisation prématurée
Patrick

1
@Patrick Bingo. Si vous allez optimiser, utilisez un profileur pour voir où se trouvent les sections lentes. Ne devinez pas. Vous pouvez généralement avoir une idée de l'emplacement des sections lentes, mais confirmez-le avec un profileur.
CHendrix

@ gnasher729 Pour résoudre ce problème particulier, il faudra plus qu'un profileur - il faudra aussi apprendre à lire le code machine démonté. Bien qu'il y ait une optimisation prématurée, il n'y a rien de tel qu'un apprentissage prématuré (au moins dans le développement de logiciels).
rwong

Vous pouvez avoir ce problème si vous appelez une fonction un million de fois, mais vous êtes plus susceptible d'avoir d'autres problèmes qui ont un impact beaucoup plus important.
Michael Shaw

5

Ceci est une question d'implémentation du compilateur ou du runtime (et de ses options) et ne peut être dit avec certitude.

Dans C et C ++, certains compilateurs insèrent des appels en fonction des paramètres d'optimisation - cela peut être vu de manière triviale en examinant l'assembly généré lors de la consultation d'outils tels que https://gcc.godbolt.org/

D'autres langages, tels que Java, font cela dans le cadre de l'exécution. Cela fait partie du JIT et est développé dans cette question SO . En particulier, regardez les options JVM pour HotSpot

-XX:InlineSmallCode=n Inlinez une méthode précédemment compilée uniquement si sa taille de code natif généré est inférieure à cela. La valeur par défaut varie selon la plate-forme sur laquelle la JVM s'exécute.
-XX:MaxInlineSize=35 Taille maximale du bytecode d'une méthode à aligner.
-XX:FreqInlineSize=n Taille maximale du bytecode d'une méthode fréquemment exécutée à inclure. La valeur par défaut varie selon la plate-forme sur laquelle la JVM s'exécute.

Alors oui, le compilateur HotSpot JIT insérera des méthodes qui répondent à certains critères.

L' impact de cela est difficile à déterminer car chaque machine virtuelle Java (ou compilateur) peut faire les choses différemment et essayer de répondre avec le trait large d'un langage est presque certainement une erreur. L'impact ne peut être correctement déterminé qu'en profilant le code dans l'environnement d'exécution approprié et en examinant la sortie compilée.

Cela peut être vu comme une approche erronée avec CPython non en ligne, mais Jython (Python fonctionnant dans la JVM) ayant certains appels en ligne. De même, MRI Ruby ne sera pas en ligne alors que JRuby le ferait, et ruby2c qui est un transpileur pour ruby ​​en C ... qui pourrait alors être en ligne ou non en fonction des options du compilateur C qui ont été compilées.

Les langues ne s'alignent pas. Les implémentations peuvent .


5

Vous recherchez des performances au mauvais endroit. Le problème avec les appels de fonction n'est pas qu'ils coûtent cher. Il y a un autre problème. Les appels de fonction pourraient être absolument gratuits et vous auriez toujours cet autre problème.

C'est qu'une fonction est comme une carte de crédit. Comme vous pouvez facilement l'utiliser, vous avez tendance à l'utiliser plus que vous ne le devriez. Supposons que vous l'appeliez 20% de plus que nécessaire. Ensuite, un gros logiciel typique contient plusieurs couches, chacune appelant des fonctions dans la couche ci-dessous, de sorte que le facteur 1,2 peut être aggravé par le nombre de couches. (Par exemple, s'il y a cinq couches et que chaque couche a un facteur de ralentissement de 1,2, le facteur de ralentissement composé est de 1,2 ^ 5 ou 2,5.) Ce n'est qu'une façon de penser.

Cela ne signifie pas que vous devez éviter les appels de fonction. Cela signifie que lorsque le code est opérationnel, vous devez savoir comment trouver et éliminer les déchets. Il existe de très bons conseils sur la façon de procéder sur les sites stackexchange. Cela donne une de mes contributions.

AJOUTÉ: Petit exemple. Une fois, j'ai travaillé dans une équipe sur un logiciel d'usine qui suivait une série de bons de travail ou "travaux". Il y avait une fonction JobDone(idJob)qui pouvait dire si un travail était fait. Un travail a été fait lorsque toutes ses sous-tâches ont été effectuées, et chacune de ces tâches a été effectuée lorsque toutes ses sous-opérations ont été effectuées. Toutes ces choses ont été enregistrées dans une base de données relationnelle. Un seul appel à une autre fonction pourrait extraire toutes ces informations, JobDoneappelées cette autre fonction, voir si le travail était terminé et jeter le reste. Ensuite, les gens pourraient facilement écrire du code comme celui-ci:

while(!JobDone(idJob)){
    ...
}

ou

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

Vous voyez le point? La fonction était si "puissante" et facile à appeler qu'elle a été trop appelée. Le problème de performances n'était donc pas les instructions d'entrée et de sortie de la fonction. C'était qu'il devait y avoir un moyen plus direct de savoir si des travaux étaient effectués. Encore une fois, ce code aurait pu être intégré à des milliers de lignes de code par ailleurs innocent. Essayer de le réparer à l'avance est ce que tout le monde essaie de faire, mais c'est comme essayer de lancer des fléchettes dans une pièce sombre. Ce dont vous avez besoin à la place, c'est de le faire fonctionner, puis laissez le "code lent" vous dire ce que c'est, simplement en prenant du temps. Pour cela, j'utilise une pause aléatoire .


1

Je pense que cela dépend vraiment de la langue et de la fonction. Alors que les compilateurs c et c ++ peuvent incorporer de nombreuses fonctions, ce n'est pas le cas pour Python ou Java.

Bien que je ne connaisse pas les détails spécifiques à Java (sauf que chaque méthode est virtuelle mais je vous suggère de mieux vérifier la documentation), en Python, je suis sûr qu'il n'y a pas d'inline, aucune optimisation de récursivité de queue et les appels de fonction sont assez chers.

Les fonctions Python sont essentiellement des objets exécutables (et en fait, vous pouvez également définir la méthode call () pour faire d'une instance d'objet une fonction). Cela signifie qu'il y a beaucoup de frais généraux pour les appeler ...

MAIS

lorsque vous définissez des variables à l'intérieur des fonctions, l'interpréteur utilise LOADFAST au lieu de l'instruction LOAD normale dans le bytecode, ce qui rend votre code plus rapide ...

Une autre chose est que lorsque vous définissez un objet appelable, des modèles comme la mémorisation sont possibles et ils peuvent effectivement accélérer considérablement votre calcul (au prix d'utiliser plus de mémoire). Fondamentalement, c'est toujours un compromis. Le coût des appels de fonction dépend également des paramètres, car ils déterminent la quantité de choses que vous devez réellement copier sur la pile (donc en c / c ++, il est courant de passer de gros paramètres comme des structures par des pointeurs / référence plutôt que par valeur).

Je pense que votre question est en pratique trop large pour recevoir une réponse complète sur stackexchange.

Ce que je vous suggère de faire est de commencer avec une langue et d'étudier la documentation avancée pour comprendre comment les appels de fonction sont implémentés par cette langue spécifique.

Vous serez surpris par le nombre de choses que vous apprendrez dans ce processus.

Si vous avez un problème spécifique, faites des mesures / profilage et décidez de la météo, il est préférable de créer une fonction ou de copier / coller le code équivalent.

si vous posez une question plus précise, il serait plus facile d'obtenir une réponse plus précise, je pense.


Je vous cite: "Je pense que votre question est en pratique trop large pour recevoir une réponse complète sur stackexchange." Comment puis-je le réduire alors? J'aimerais voir des données réelles représentant l'impact des appels de fonction dans les performances. Peu m'importe la langue, je suis simplement curieux de voir une explication plus détaillée, étayée si possible par des données, comme je l'ai dit.
dabadaba

Le fait est que cela dépend de la langue. En C et C ++, si la fonction est en ligne, l'impact est 0. Si elle n'est pas en ligne, elle dépend de ses paramètres, si elle est dans le cache ou non, etc ...
ingframin

1

Il y a quelque temps, j'ai mesuré les frais généraux des appels de fonction C ++ directs et virtuels sur le Xenon PowerPC .

Les fonctions en question avaient un seul paramètre et un seul retour, donc le passage des paramètres s'est produit sur les registres.

Pour faire court, la surcharge d'un appel de fonction direct (non virtuel) était d'environ 5,5 nanosecondes, ou 18 cycles d'horloge, par rapport à un appel de fonction en ligne. La surcharge d'un appel de fonction virtuelle était de 13,2 nanosecondes, ou 42 cycles d'horloge, par rapport à l'inline.

Ces synchronisations sont probablement différentes selon les différentes familles de processeurs. Mon code de test est ici ; vous pouvez exécuter la même expérience sur votre matériel. Utilisez un minuteur de haute précision comme rdtsc pour votre implémentation CFastTimer; l'heure système () n'est pas assez précise.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.