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 foo
Name bezieht, sodass das Ausgabeprogramm direkt zu der foo
Funktion 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 bar
ruft die Funktion ihr Argument auf f
, was alles sein kann. Daher kann der Compiler nicht einfach bar
zu 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 foo
Eigenschaft im y
Objekt abgefragt und alles aufgerufen wird , was er findet. Es ist nicht bekannt, ob y
eine Klasse vorhanden sein A
wird oder ob die A
Klasse eine foo
Methode 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 y
auf Klassen A
mithilfe 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 y
möglicherweise eine andere (Unter-) Klasse gibt, sodass wir zusätzliche Informationen wie Java- final
Annotationen benötigen , um genau zu wissen, welche Funktion aufgerufen wird.
Haskell ist keine OO - Sprache, aber wir können den Wert ableiten f
von inlining bar
(welches statisch versendet) in main
, unter Substitution foo
für y
. Da das Ziel von foo
in main
statisch 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.