it-swarm.com.de

Wie stark wirken sich Funktionsaufrufe auf die Leistung aus?

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 ...

13
dabadaba

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 beschließt, DoSomethingElse inline zu setzen, und 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.

21
CHendrix

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 JIT und wird in this SO Frage . Schauen Sie sich insbesondere die JVM-Optionen für HotSpot an

-XX:InlineSmallCode=n Inline eine zuvor kompilierte 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 Auswirkung davon ist schwer zu bestimmen, da jede JVM (oder jeder Compiler) die Dinge möglicherweise anders macht und versucht, mit dem breiten Strich einer Sprache zu antworten fast sicher falsch. 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 MRI Ruby nicht inlining, während JRuby dies tun würde, und Ruby2c, ein Transpiler für Ruby in C ..., der dann inlining sein könnte oder nicht, abhängig von der C Compiler-Optionen, mit denen kompiliert wurde.

Sprachen sind nicht inline. Implementierungen können .

5
user227864

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 erhöht 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 ein Job erledigt wurde. 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, also rief JobDone diese andere Funktion auf, sah, ob die Arbeit erledigt war, und warf den Rest weg. 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. Was Sie stattdessen brauchen, ist, es zum Laufen zu bringen, und dann lassen Sie sich vom "langsamen Code" sagen, was es ist, indem Sie sich einfach Zeit nehmen. Dafür benutze ich zufällige Pause .

5
Mike Dunlavey

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.

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

Python-Funktionen sind im Grunde ausführbare Objekte (und tatsächlich können Sie auch die Methode call () 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.

1
ingframin

Ich habe vor einiger Zeit den Overhead von direkten und virtuellen C++ - Funktionsaufrufen 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 im Vergleich zu einem Inline-Funktionsaufruf ungefähr 5,5 Nanosekunden oder 18 Taktzyklen. 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.

1
Crashworks