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 legate alla tua domanda. Innanzitutto, chiedi:
I costi delle chiamate di funzione sono ancora importanti nei compilatori moderni?
Notare le parole chiave "funzione" e "compilatori". Il tuo preventivo è leggermente diverso:
Ricorda che il costo di una chiamata di metodo può essere significativo, a seconda della lingua.
Si tratta di metodi , nel senso orientato agli oggetti.
Mentre "funzione" e "metodo" sono spesso usati in modo intercambiabile, ci sono differenze quando si tratta del loro costo (di cui stai chiedendo) e quando si tratta di compilazione (che è il contesto che hai dato).
In particolare, abbiamo bisogno di conoscere la spedizione statica vs spedizione dinamica . Per il momento ignorerò le ottimizzazioni.
In un linguaggio come C, di solito chiamiamo funzioni con invio statico . Per 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 foosi riferisce quel nome, quindi il programma di output può passare direttamente alla foofunzione, che è abbastanza economica. Questo è ciò che significa invio statico .
L'alternativa è l' invio dinamico , in cui il compilatore non sa quale funzione viene chiamata. Ad esempio, ecco un po 'di codice Haskell (poiché l'equivalente C sarebbe disordinato!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Qui la barfunzione chiama il suo argomento f, che potrebbe essere qualsiasi cosa. Quindi il compilatore non può semplicemente compilare barun'istruzione di salto veloce, perché non sa dove saltare. Invece, il codice per cui generiamo barfarà la differenza fper scoprire a quale funzione sta puntando, quindi passa ad essa. Questo è ciò che significa spedizione dinamica .
Entrambi questi esempi sono per funzioni . Hai menzionato i metodi , che possono essere considerati come uno stile particolare della funzione inviata in modo dinamico. 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 y.foo()chiamata utilizza l'invio dinamico, poiché cerca il valore della fooproprietà ynell'oggetto e chiama qualunque cosa trovi; non sa che yavrà classe Ao che la Aclasse contiene un foometodo, quindi non possiamo semplicemente saltare direttamente ad essa.
OK, questa è l'idea di base. Si noti che l'invio statico è più veloce dell'invio dinamico indipendentemente dal fatto che compiliamo o interpretiamo; tutto il resto è uguale. La dereferenziazione comporta un costo aggiuntivo in entrambi i casi.
In che modo ciò influisce sui compilatori moderni e ottimizzati?
La prima cosa da notare è che l'invio statico può essere ottimizzato in modo più pesante: quando sappiamo a quale funzione stiamo saltando, possiamo fare cose come l'allineamento. Con l'invio dinamico, non sappiamo che stiamo saltando fino al runtime, quindi non c'è molta ottimizzazione che possiamo fare.
In secondo luogo, in alcune lingue è possibile dedurre dove finiranno alcuni invii dinamici e quindi ottimizzarli in invii statici. Questo ci consente di eseguire altre ottimizzazioni come inline, ecc.
Nell'esempio precedente di Python tale inferenza è piuttosto senza speranza, dal momento che Python consente ad altro codice di sovrascrivere classi e proprietà, quindi è difficile dedurre molto che si terrà in tutti i casi.
Se la nostra lingua ci consente di imporre più restrizioni, ad esempio limitando yalla classe Autilizzando un'annotazione, allora potremmo usare tali informazioni per inferire la funzione target. Nelle lingue con la sottoclasse (che è quasi tutte le lingue con le classi!) Questo in realtà non è abbastanza, dal momento che ypotrebbe effettivamente avere una (sotto) classe diversa, quindi avremmo bisogno di informazioni extra come le finalannotazioni di Java per sapere esattamente quale funzione verrà chiamata.
Haskell non è un linguaggio OO, ma possiamo dedurre il valore di finline bar(che viene inviato staticamente ) in mainsostituzione foodi y. Poiché il target di fooin mainè staticamente noto, la chiamata viene inviata staticamente e probabilmente verrà incorporata e ottimizzata completamente (poiché queste funzioni sono piccole, è più probabile che il compilatore li incorpori; sebbene non possiamo contare su quello in generale ).
Quindi il costo si riduce a:
- La lingua invia la chiamata in modo statico o dinamico?
- Se è quest'ultimo, il linguaggio consente all'implementazione di dedurre il target usando altre informazioni (ad esempio tipi, classi, annotazioni, inline, ecc.)?
- In che modo è possibile ottimizzare la spedizione statica (dedotta o meno)?
Se stai usando un linguaggio "molto dinamico", con un sacco di invio dinamico 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 sia intelligente l'implementazione.