Wie stark wirken sich Funktionsaufrufe auf die Leistung aus?

13

Das Extrahieren von Funktionen in Methoden oder Funktionen ist ein Muss für Codemodularität, Lesbarkeit und Interoperabilität, insbesondere in OOP.

Dies bedeutet jedoch, dass mehr Funktionsaufrufe getätigt werden.

Wie wirkt sich die Aufteilung unseres Codes in Methoden oder Funktionen tatsächlich auf die Leistung in modernen * Sprachen aus?

* Die beliebtesten: C, Java, C ++, C #, Python, JavaScript, Ruby ...

Dabadaba
quelle
1
Ich denke, jede Sprachimplementierung, die ihr Geld wert ist, macht seit mehreren Jahrzehnten Inlining. IOW, der Overhead ist genau 0.
Jörg W Mittag
1
"Es werden mehr Funktionsaufrufe getätigt" ist häufig nicht der Fall, da bei vielen dieser Aufrufe der Overhead durch die verschiedenen Compiler / Interpreter optimiert wird, die Ihren Code verarbeiten und Inhalte einbinden. Wenn Ihre Sprache diese Art von Optimierungen nicht hat, halte ich sie möglicherweise nicht für modern.
Ixrec
2
Wie wird sich dies auf die Leistung auswirken? Es wird entweder schneller oder langsamer oder es wird nicht geändert, je nachdem, welche spezifische Sprache Sie verwenden und wie der eigentliche Code aufgebaut ist und möglicherweise welche Version des Compilers Sie verwenden und möglicherweise sogar welche Plattform Sie verwenden. läuft weiter. Jede Antwort, die Sie erhalten, wird eine Variation dieser Unsicherheit sein, mit mehr Worten und mehr unterstützenden Beweisen.
GrandOpener
1
Die Auswirkungen, wenn überhaupt, sind so klein , dass man eine Person, wird nie jemals bemerken. Es gibt andere weitaus wichtigere Dinge, über die man sich Sorgen machen muss. Wie die, ob Tabulatoren 5 oder 7 Leerzeichen sein sollten.
MetaFight

Antworten:

21

Könnte sein. Der Compiler könnte entscheiden, "hey, diese Funktion wird nur ein paar Mal aufgerufen, und ich soll die Geschwindigkeit optimieren, also werde ich diese Funktion einfach einbinden". Im Wesentlichen ersetzt der Compiler den Funktionsaufruf durch den Hauptteil der Funktion. Zum Beispiel würde der Quellcode so aussehen.

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

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

Der Compiler entscheidet sich für Inline DoSomethingElseund der Code wird

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

Wenn Funktionen nicht inline sind, gibt es einen Leistungseinbruch, um einen Funktionsaufruf durchzuführen. Es ist jedoch ein so winziger Treffer, dass sich nur extrem leistungsstarker Code um Funktionsaufrufe sorgen muss. Bei solchen Projekten wird der Code normalerweise in Assembly geschrieben.

Funktionsaufrufe (abhängig von der Plattform) umfassen normalerweise einige 10 Sekunden Anweisungen, einschließlich des Speicherns / Wiederherstellens des Stapels. Einige Funktionsaufrufe bestehen aus einer Sprung- und Rückgabeanweisung.

Es gibt jedoch noch andere Faktoren, die sich auf die Leistung von Funktionsaufrufen auswirken können. Die aufgerufene Funktion wird möglicherweise nicht in den Cache des Prozessors geladen, was zu einem Cache-Fehler führt und den Speichercontroller zwingt, die Funktion aus dem Hauptspeicher abzurufen. Dies kann einen großen Leistungseinbruch verursachen.

Kurz gesagt: Funktionsaufrufe können die Leistung beeinträchtigen oder nicht. Die einzige Möglichkeit, dies festzustellen, besteht darin, Ihren Code zu profilieren. Versuchen Sie nicht zu erraten, wo sich die langsamen Code-Spots befinden, da der Compiler und die Hardware einige unglaubliche Tricks auf Lager haben. Profilieren Sie den Code, um die Position der langsamen Stellen zu ermitteln.

CHendrix
quelle
1
Ich habe mit modernen Compilern (gcc, clang) in Situationen gesehen, in denen es mir wirklich wichtig war, dass sie ziemlich schlechten Code für Schleifen innerhalb einer großen Funktion erstellt haben . Das Extrahieren der Schleife in eine statische Funktion hat aufgrund von Inlining nicht geholfen. Durch Extrahieren der Schleife in eine externe Funktion wurden in einigen Fällen signifikante (in Benchmarks messbare) Geschwindigkeitsverbesserungen erzielt.
Gnasher729
1
Ich würde darauf zurückgreifen und sagen, OP sollte bei vorzeitiger Optimierung
Patrick
1
@ Patrick Bingo. Wenn Sie optimieren möchten, verwenden Sie einen Profiler, um zu sehen, wo sich die langsamen Abschnitte befinden. Raten Sie nicht. Normalerweise können Sie ein Gefühl dafür bekommen, wo sich die langsamen Abschnitte befinden könnten, aber bestätigen Sie dies mit einem Profiler.
CHendrix
@ gnasher729 Um dieses spezielle Problem zu lösen, benötigt man mehr als einen Profiler - man muss auch lernen, den zerlegten Maschinencode zu lesen. Während es eine vorzeitige Optimierung gibt, gibt es kein vorzeitiges Lernen (zumindest in der Softwareentwicklung).
Rwong
Sie können dieses Problem haben , wenn Sie eine Funktion eine Million Mal anrufen, aber sie sind eher andere Probleme haben , die einen deutlich größeren Einfluss haben.
Michael Shaw
5

Dies ist eine Frage der Implementierung des Compilers oder der Laufzeit (und ihrer Optionen) und kann nicht mit Sicherheit gesagt werden.

In C und C ++ werden einige Compiler Aufrufe basierend auf Optimierungseinstellungen inline ausführen. Dies lässt sich trivial erkennen, wenn Sie die generierte Assembly untersuchen, wenn Sie Tools wie https://gcc.godbolt.org/ betrachten.

Andere Sprachen wie Java haben dies als Teil der Laufzeit. Dies ist Teil der GEG und wird in dieser SO-Frage näher erläutert . Schauen Sie sich insbesondere die JVM-Optionen für HotSpot an

-XX:InlineSmallCode=n Inline einer zuvor kompilierten Methode nur, wenn ihre generierte native Codegröße kleiner als diese ist. Der Standardwert hängt von der Plattform ab, auf der die JVM ausgeführt wird.
-XX:MaxInlineSize=35 Maximale Bytecode-Größe einer Methode, die eingebunden werden soll.
-XX:FreqInlineSize=n Maximale Bytecode-Größe einer häufig ausgeführten Methode, die eingebunden werden soll. Der Standardwert hängt von der Plattform ab, auf der die JVM ausgeführt wird.

Ja, der HotSpot JIT-Compiler integriert Methoden, die bestimmte Kriterien erfüllen.

Die Auswirkungen sind schwer zu bestimmen, da jede JVM (oder jeder Compiler) die Dinge möglicherweise anders macht und der Versuch, mit dem breiten Strich einer Sprache zu antworten, mit ziemlicher Sicherheit falsch ist. Die Auswirkungen können nur richtig ermittelt werden, indem der Code in der entsprechenden laufenden Umgebung profiliert und die kompilierte Ausgabe untersucht wird.

Dies kann als fehlgeleiteter Ansatz angesehen werden, bei dem CPython nicht inline ist, sondern bei Jython (Python, das in der JVM ausgeführt wird) einige Aufrufe inline sind. Ebenso wird MRI Ruby nicht inliniert, während JRuby dies tun würde, und ruby2c, ein Transpiler für Ruby in C ..., der dann inline sein könnte oder nicht, abhängig von den C-Compileroptionen, mit denen kompiliert wurde.

Sprachen sind nicht inline. Implementierungen können .

user227864
quelle
5

Sie suchen Leistung am falschen Ort. Das Problem bei Funktionsaufrufen ist nicht, dass sie viel kosten. Es gibt noch ein anderes Problem. Funktionsaufrufe könnten absolut kostenlos sein, und Sie hätten immer noch dieses andere Problem.

Es ist so, dass eine Funktion wie eine Kreditkarte ist. Da Sie es leicht verwenden können, neigen Sie dazu, es mehr zu verwenden, als Sie vielleicht sollten. Angenommen, Sie nennen es 20% mehr als nötig. Dann enthält eine typische große Software mehrere Ebenen, wobei jede aufrufende Funktion in der darunter liegenden Ebene ausgeführt wird, sodass der Faktor 1,2 durch die Anzahl der Ebenen zusammengesetzt werden kann. (Wenn beispielsweise fünf Schichten vorhanden sind und jede Schicht einen Verlangsamungsfaktor von 1,2 aufweist, beträgt der zusammengesetzte Verlangsamungsfaktor 1,2 ^ 5 oder 2,5.) Dies ist nur eine Möglichkeit, darüber nachzudenken.

Dies bedeutet nicht, dass Sie Funktionsaufrufe vermeiden sollten. Wenn der Code ausgeführt wird, sollten Sie wissen, wie Sie den Abfall finden und beseitigen können. Es gibt viele ausgezeichnete Ratschläge dazu auf Stackexchange-Sites. Dies gibt einen meiner Beiträge.

HINZUGEFÜGT: Kleines Beispiel. Einmal arbeitete ich in einem Team an Fabriksoftware, die eine Reihe von Arbeitsaufträgen oder "Jobs" verfolgte. Es gab eine Funktion JobDone(idJob), die erkennen konnte, ob eine Arbeit erledigt war. Eine Arbeit wurde erledigt, wenn alle ihre Unteraufgaben erledigt waren, und jede dieser Aufgaben wurde erledigt, wenn alle ihre Unteroperationen erledigt waren. All diese Dinge wurden in einer relationalen Datenbank verfolgt. Ein einzelner Aufruf einer anderen Funktion könnte all diese Informationen extrahieren, die so JobDonegenannte andere Funktion, sehen, ob die Arbeit erledigt ist, und den Rest wegwerfen. Dann könnten die Leute leicht Code wie diesen schreiben:

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

oder

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

Sehen Sie den Punkt? Die Funktion war so "mächtig" und einfach aufzurufen, dass sie viel zu oft aufgerufen wurde. Das Leistungsproblem waren also nicht die Anweisungen, die in die Funktion ein- und ausgehen. Es musste einen direkteren Weg geben, um festzustellen, ob Arbeiten erledigt waren. Auch dieser Code könnte in Tausende von Zeilen ansonsten unschuldigen Codes eingebettet sein. Der Versuch, das Problem im Voraus zu beheben, ist das, was jeder versucht, aber das ist wie der Versuch, Pfeile in einen dunklen Raum zu werfen. Stattdessen müssen Sie es zum Laufen bringen und sich dann vom "langsamen Code" sagen lassen, was es ist, indem Sie sich einfach Zeit nehmen. Dafür benutze ich zufällige Pausen .

Mike Dunlavey
quelle
1

Ich denke, es hängt wirklich von der Sprache und der Funktion ab. Während die Compiler c und c ++ viele Funktionen einbinden können, ist dies bei Python oder Java nicht der Fall.

Obwohl ich die spezifischen Details für Java nicht kenne (außer dass jede Methode virtuell ist, aber ich empfehle Ihnen, die Dokumentation besser zu überprüfen), bin ich mir sicher, dass es in Python kein Inlining gibt, keine Optimierung der Schwanzrekursion und Funktionsaufrufe ziemlich teuer sind.

Python-Funktionen sind im Grunde ausführbare Objekte (und tatsächlich können Sie auch die call () -Methode definieren, um eine Objektinstanz zu einer Funktion zu machen). Dies bedeutet, dass es ziemlich viel Aufwand bedeutet, sie anzurufen ...

ABER

Wenn Sie Variablen in Funktionen definieren, verwendet der Interpreter LOADFAST anstelle der normalen LOAD-Anweisung im Bytecode, wodurch Ihr Code schneller wird ...

Eine andere Sache ist, dass beim Definieren eines aufrufbaren Objekts Muster wie das Auswendiglernen möglich sind und Ihre Berechnung erheblich beschleunigen können (auf Kosten der Verwendung von mehr Speicher). Grundsätzlich ist es immer ein Kompromiss. Die Kosten für Funktionsaufrufe hängen auch von den Parametern ab, da sie bestimmen, wie viel Material Sie tatsächlich auf den Stapel kopieren müssen (daher ist es in c / c ++ üblich, große Parameter wie Strukturen als Zeiger / Referenz anstatt als Wert zu übergeben).

Ich denke, dass Ihre Frage in der Praxis zu weit gefasst ist, um beim Stapelaustausch vollständig beantwortet zu werden.

Ich empfehle Ihnen, mit einer Sprache zu beginnen und die erweiterte Dokumentation zu studieren, um zu verstehen, wie Funktionsaufrufe von dieser bestimmten Sprache implementiert werden.

Sie werden überrascht sein, wie viele Dinge Sie in diesem Prozess lernen werden.

Wenn Sie ein bestimmtes Problem haben, führen Sie Messungen / Profile durch und entscheiden Sie, ob es besser ist, eine Funktion zu erstellen oder den entsprechenden Code zu kopieren / einzufügen.

Wenn Sie eine spezifischere Frage stellen, ist es meiner Meinung nach einfacher, eine spezifischere Antwort zu erhalten.

Ingframin
quelle
Ich zitiere Sie: "Ich denke, Ihre Frage ist in der Praxis zu weit gefasst, um beim Stapelaustausch vollständig beantwortet zu werden." Wie kann ich es dann eingrenzen? Ich würde gerne einige tatsächliche Daten sehen, die die Auswirkungen von Funktionsaufrufen auf die Leistung darstellen. Es ist mir egal, welche Sprache, ich bin nur neugierig auf eine detailliertere Erklärung, die, wenn möglich, mit Daten gesichert ist, wie gesagt.
Dabadaba
Der Punkt ist, dass es von der Sprache abhängt. Wenn in C und C ++ die Funktion inline ist, ist die Auswirkung 0. Wenn sie nicht inline ist, hängt sie von ihren Parametern ab, ob sie sich im Cache befindet oder nicht usw.
ingframin
1

Ich habe vor einiger Zeit den Overhead direkter und virtueller C ++ - Funktionsaufrufe auf dem Xenon PowerPC gemessen .

Die fraglichen Funktionen hatten einen einzelnen Parameter und eine einzelne Rückgabe, so dass die Parameterübergabe in Registern erfolgte.

Kurz gesagt, der Overhead eines direkten (nicht virtuellen) Funktionsaufrufs betrug ungefähr 5,5 Nanosekunden oder 18 Taktzyklen im Vergleich zu einem Inline-Funktionsaufruf. Der Overhead eines virtuellen Funktionsaufrufs betrug 13,2 Nanosekunden oder 42 Taktzyklen im Vergleich zu Inline.

Diese Timings unterscheiden sich wahrscheinlich in verschiedenen Prozessorfamilien. Mein Testcode ist hier ; Sie können das gleiche Experiment auf Ihrer Hardware ausführen. Verwenden Sie für Ihre CFastTimer-Implementierung einen hochpräzisen Timer wie rdtsc . Die Systemzeit () ist bei weitem nicht genau genug.

Crashworks
quelle