Wie andere sagen, sollten Sie zuerst die Leistung Ihres Programms messen und werden in der Praxis wahrscheinlich keinen Unterschied feststellen.
Aus konzeptioneller Sicht dachte ich jedoch, ich würde ein paar Dinge klären, die in Ihrer Frage zusammenfließen. Zunächst fragen Sie:
Sind Funktionsaufrufkosten in modernen Compilern immer noch wichtig?
Beachten Sie die Schlüsselwörter "function" und "compilers". Ihr Zitat ist subtil anders:
Beachten Sie, dass die Kosten eines Methodenaufrufs je nach Sprache erheblich sein können.
Hierbei handelt es sich um Methoden im objektorientierten Sinne.
Während "function" und "method" häufig synonym verwendet werden, gibt es Unterschiede hinsichtlich der Kosten (nach denen Sie fragen) und der Kompilierung (nach dem von Ihnen angegebenen Kontext).
Insbesondere müssen wir den statischen Versand im Vergleich zum dynamischen Versand kennen . Ich werde Optimierungen für den Moment ignorieren.
In einer Sprache wie C rufen wir normalerweise Funktionen mit statischem Versand auf . Zum Beispiel:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
Wenn der Compiler den Aufruf sieht foo(y), weiß er, auf welche Funktion sich dieser fooName bezieht, sodass das Ausgabeprogramm direkt zu der fooFunktion springen kann , die recht billig ist. Das ist , was statische Dispatch bedeutet.
Die Alternative ist der dynamische Versand , bei dem der Compiler nicht weiß, welche Funktion aufgerufen wird. Hier ist ein Beispiel für einen Haskell-Code (da das C-Äquivalent chaotisch wäre!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Hier barruft die Funktion ihr Argument auf f, was alles sein kann. Daher kann der Compiler nicht einfach barzu einer schnellen Sprunganweisung kompilieren , da er nicht weiß, wohin er springen soll. Stattdessen wird der Code, für den wir generieren bar, dereferenziert f, um herauszufinden, auf welche Funktion er zeigt, und dann zu dieser zu springen. Das bedeutet dynamischer Versand .
Beide Beispiele beziehen sich auf Funktionen . Sie haben Methoden erwähnt , die als ein bestimmter Stil einer dynamisch versendeten Funktion angesehen werden können. Hier ist zum Beispiel Python:
class A:
def __init__(self, x):
self.x = x
def foo(self):
return self.x + 1
def bar(y):
return y.foo()
z = A(42)
bar(z)
Der y.foo()Aufruf verwendet den dynamischen Versand, da der Wert der fooEigenschaft im yObjekt abgefragt und alles aufgerufen wird , was er findet. Es ist nicht bekannt, ob yeine Klasse vorhanden sein Awird oder ob die AKlasse eine fooMethode enthält , daher können wir nicht direkt zu ihr springen.
OK, das ist die Grundidee. Beachten Sie, dass der statische Versand schneller ist als der dynamische Versand, unabhängig davon, ob er kompiliert oder interpretiert wird. alles andere ist gleich. Für die Dereferenzierung fallen in beiden Fällen zusätzliche Kosten an.
Wie wirkt sich das auf moderne, optimierte Compiler aus?
Das Erste, was zu beachten ist, ist, dass der statische Versand stärker optimiert werden kann: Wenn wir wissen, zu welcher Funktion wir springen, können wir Dinge wie Inlining tun. Beim dynamischen Versand wissen wir nicht, dass wir erst zur Laufzeit springen, daher können wir nicht viel optimieren.
Zweitens ist es in einigen Sprachen möglich, abzuleiten, wohin einige dynamische Versendungen springen, und sie daher zu statischen Versendungen zu optimieren. Auf diese Weise können wir weitere Optimierungen wie Inlining usw. durchführen.
In dem obigen Python-Beispiel ist eine solche Folgerung ziemlich hoffnungslos, da Python zulässt, dass anderer Code Klassen und Eigenschaften überschreibt, sodass es schwierig ist, auf vieles zu schließen, was in allen Fällen Bestand hat.
Wenn unsere Sprache uns mehr Einschränkungen auferlegen lässt, zum Beispiel durch Beschränkung yauf Klassen Amithilfe einer Annotation, könnten wir diese Informationen verwenden, um auf die Zielfunktion zu schließen. In Sprachen mit Unterklassen (das sind fast alle Sprachen mit Klassen!) Reicht das eigentlich nicht aus, da es ymöglicherweise eine andere (Unter-) Klasse gibt, sodass wir zusätzliche Informationen wie Java- finalAnnotationen benötigen , um genau zu wissen, welche Funktion aufgerufen wird.
Haskell ist keine OO - Sprache, aber wir können den Wert ableiten fvon inlining bar(welches statisch versendet) in main, unter Substitution foofür y. Da das Ziel von fooin mainstatisch bekannt ist, wird der Aufruf statisch weitergeleitet und wird wahrscheinlich vollständig eingebunden und optimiert (da diese Funktionen klein sind, werden sie vom Compiler eher eingebunden, obwohl wir uns im Allgemeinen nicht darauf verlassen können ).
Daher belaufen sich die Kosten auf:
- Versendet die Sprache Ihren Anruf statisch oder dynamisch?
- Kann die Implementierung in letzterem Fall auf andere Informationen (z. B. Typen, Klassen, Anmerkungen, Inlining usw.) zurückgreifen?
- Wie aggressiv kann der statische Versand (abgeleitet oder anderweitig) optimiert werden?
Wenn Sie eine "sehr dynamische" Sprache mit viel dynamischem Versand und wenigen Garantien verwenden, die dem Compiler zur Verfügung stehen, fallen für jeden Aufruf Kosten an. Wenn Sie eine "sehr statische" Sprache verwenden, wird ein ausgereifter Compiler sehr schnellen Code erzeugen. Wenn Sie dazwischen sind, kann dies von Ihrem Codierungsstil und der Art der Implementierung abhängen.