Come altri dicono, dovresti prima misurare le prestazioni del tuo programma e probabilmente non troverai alcuna differenza nella pratica.
Tuttavia, da un livello concettuale ho pensato di chiarire alcune cose che sono confuse nella tua domanda. Innanzitutto, chiedi:
Do function call costs still matter in modern compilers?
Notare le parole chiave "funzione" e "compilatori". La tua offerta è molto diversa:
Remember that the cost of a method call can be significant, depending on the language.
Si tratta di metodi , nel senso orientato agli oggetti.
Anche se "funzione" e "metodo" sono spesso usati in modo intercambiabile, ci sono delle differenze quando si tratta del loro costo (che stai chiedendo) e quando si tratta di compilazione (che è il contesto che hai dato).
In particolare, dobbiamo sapere su dispatch statico vs dispatch dinamico . Ignorerò le ottimizzazioni per il momento.
In una lingua come C, solitamente chiamiamo funzioni con dispatch statico . Ad esempio:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
Quando il compilatore vede la chiamata foo(y)
, sa a quale funzione si riferisce il nome foo
, quindi il programma di output può passare direttamente alla funzione foo
, che è piuttosto economica. Questo è ciò che invio statico significa.
L'alternativa è dispatch dinamico , dove il compilatore non conosce quale funzione viene chiamata. Ad esempio, ecco un codice Haskell (poiché l'equivalente C sarebbe disordinato!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Qui la funzione bar
chiama il suo argomento f
, che potrebbe essere qualsiasi cosa. Quindi il compilatore non può semplicemente compilare bar
con un'istruzione di salto veloce, perché non sa dove andare. Invece, il codice che generiamo per bar
sarà dereferenziato f
per scoprire a quale funzione sta puntando, quindi saltare ad esso. Questo è ciò che invio dinamico significa.
Entrambi questi esempi sono per funzioni . Hai citato i metodi , che possono essere pensati come uno stile particolare di funzioni inviate dinamicamente. Ad esempio, ecco alcuni 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)
La chiamata y.foo()
utilizza l'invio dinamico, dal momento che sta cercando il valore della proprietà foo
nell'oggetto y
e chiama ciò che trova; non sa che y
avrà classe A
, o che la classe A
contenga un metodo foo
, quindi non possiamo semplicemente passare direttamente ad essa.
OK, questa è l'idea di base. Nota che il dispatch statico è più veloce della dispatch dinamica a prescindere se compiliamo o interpretiamo; tutto il resto è uguale. Il dereferenziamento comporta un costo aggiuntivo in ogni caso.
Quindi, in che modo questo influisce sui moderni, ottimizzando i compilatori?
La prima cosa da notare è che il dispatch statico può essere ottimizzato più pesantemente: quando sappiamo a quale funzione stiamo saltando, possiamo fare cose come l'inlining. Con la spedizione dinamica, non sappiamo che stiamo saltando fino al tempo di esecuzione, quindi non possiamo fare molta ottimizzazione.
In secondo luogo, è possibile in alcune lingue inferire dove alcune disposi- zioni dinamiche finiranno di saltare e quindi ottimizzarle in un invio statico. Questo ci consente di eseguire altre ottimizzazioni come l'inlining, ecc.
Nell'esempio di Python sopra riportato tale inferenza è piuttosto senza speranza, poiché Python consente ad altri codici di sovrascrivere classi e proprietà, quindi è difficile dedurre molto di ciò che verrà conservato in tutti i casi.
Se il nostro linguaggio ci consente di imporre ulteriori restrizioni, ad esempio limitando y
alla classe A
utilizzando un'annotazione, potremmo utilizzare tali informazioni per dedurre la funzione di destinazione. Nelle lingue con sottoclassi (che è quasi tutte le lingue con classi!) Che in realtà non è sufficiente, dal momento che y
potrebbe effettivamente avere una classe (secondaria) diversa, quindi avremmo bisogno di informazioni extra come le annotazioni final
di Java per sapere esattamente quale la funzione verrà richiamata.
Haskell non è un linguaggio OO, ma possiamo dedurre il valore di f
inserendo bar
(che è staticamente inviato) in main
, sostituendo foo
per% codice%. Poiché il target di y
in foo
è noto staticamente, la chiamata diventa staticamente inviata e probabilmente verrà completamente integrata e ottimizzata (poiché queste funzioni sono piccole, è più probabile che il compilatore li incorpori, anche se possiamo contateci in generale).
Quindi il costo si riduce a:
- La lingua invia la chiamata in modo statico o dinamico?
- Se è il secondo, la lingua consente all'implementazione di dedurre il target utilizzando altre informazioni (ad esempio tipi, classi, annotazioni, inlining, ecc.)?
- Quanto può essere ottimizzata la spedizione statica (dedotta o altrimenti)?
Se stai utilizzando un linguaggio "molto dinamico", con molta dispedizione dinamica e poche garanzie disponibili per il compilatore, ogni chiamata avrà un costo. Se stai usando un linguaggio "molto statico", un compilatore maturo produrrà un codice molto veloce. Se sei nel mezzo, allora può dipendere dal tuo stile di codifica e da quanto è intelligente l'implementazione.