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 ...
Antworten:
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.
Der Compiler entscheidet sich für Inline
DoSomethingElse
und der Code wirdWenn 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.
quelle
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
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 .
quelle
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 soJobDone
genannte andere Funktion, sehen, ob die Arbeit erledigt ist, und den Rest wegwerfen. Dann könnten die Leute leicht Code wie diesen schreiben:oder
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 .
quelle
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.
quelle
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.
quelle