web-dev-qa-db-fra.com

Dans quelle mesure les appels de fonction ont-ils un impact sur les performances?

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 le fractionnement de notre code en méthodes ou fonctions affecte-t-il réellement les performances dans les langues modernes *?

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

13
dabadaba

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 d'inline DoSomethingElse, et 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 dans Assembly.

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.

21
CHendrix

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

En 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 l'examen d'outils tels que https://gcc.godbolt.org/

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

-XX:InlineSmallCode=n Inline 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 plateforme sur laquelle la JVM s'exécute.
-XX:MaxInlineSize=35 Taille maximale du bytecode d'une méthode à insérer.
-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 plateforme 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 ceci est difficile à déterminer car chaque JVM (ou compilateur) peut faire les choses différemment et essayer de répondre avec le trait large d'un langage est presque une certitude erronée. 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, l'IRM Ruby pas en ligne alors que JRuby le ferait, et Ruby2c qui est un transpilateur pour Ruby en C ... qui pourrait alors être en ligne ou non en fonction de la Options du compilateur C qui a été compilé avec.

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

5
user227864

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. Ce 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 avait été effectué. 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 pouvait extraire toutes ces informations, donc JobDone appelait cette autre fonction, voyait si le travail était terminé et jetait le reste. Ensuite, les gens pourraient facilement écrire du code comme ceci:

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é appelée beaucoup trop. 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 est de le faire fonctionner, et puis laissez le "code lent" vous dire ce que c'est, simplement en prenant du temps. Pour cela, j'utilise pause aléatoire .

5
Mike Dunlavey

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 de Java (sauf que chaque méthode est virtuelle mais je vous suggère de mieux vérifier la documentation), dans Python je suis sûr qu'il n'y a pas d'inline, pas d'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 être répondue complètement 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.

1
ingframin

J'ai mesuré la surcharge des appels de fonction C++ directs et virtuels sur le Xenon PowerPC il y a quelque temps .

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 horaires sont probablement différents 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.

1
Crashworks