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 foo
si riferisce quel nome, quindi il programma di output può passare direttamente alla foo
funzione, 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 bar
funzione chiama il suo argomento f
, che potrebbe essere qualsiasi cosa. Quindi il compilatore non può semplicemente compilare bar
un'istruzione di salto veloce, perché non sa dove saltare. Invece, il codice per cui generiamo bar
farà la differenza f
per 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 foo
proprietà y
nell'oggetto e chiama qualunque cosa trovi; non sa che y
avrà classe A
o che la A
classe contiene un foo
metodo, 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 y
alla classe A
utilizzando 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 y
potrebbe effettivamente avere una (sotto) classe diversa, quindi avremmo bisogno di informazioni extra come le final
annotazioni di Java per sapere esattamente quale funzione verrà chiamata.
Haskell non è un linguaggio OO, ma possiamo dedurre il valore di f
inline bar
(che viene inviato staticamente ) in main
sostituzione foo
di y
. Poiché il target di foo
in 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.