I metodi virtuali sono comunemente implementati tramite le cosiddette tabelle dei metodi virtuali (vtable in breve), in cui sono memorizzati i puntatori di funzione. Questo aggiunge indiretta alla chiamata reale (devi recuperare l'indirizzo della funzione per chiamare dal vtable, quindi chiamarlo - invece di chiamarlo proprio davanti a te). Certo, questo richiede un po 'di tempo e un po' di codice.
Tuttavia, non è necessariamente la causa principale della lentezza. Il vero problema è che il compilatore (in genere / di solito) non può sapere quale funzione verrà chiamata. Quindi non può indicarlo o eseguire altre ottimizzazioni del genere. Questo da solo potrebbe aggiungere una dozzina di istruzioni inutili (preparare registri, chiamare, quindi ripristinare lo stato in seguito) e potrebbe inibire altre ottimizzazioni apparentemente non correlate. Inoltre, se branchi come matti chiamando molte implementazioni differenti, subisci gli stessi colpi che subiresti per ramificarti come un matto con altri mezzi: il predittore di cache e di ramo non ti aiuterà, i rami impiegheranno più tempo di un perfettamente prevedibile ramo.
Grande ma : questi risultati delle prestazioni sono in genere troppo piccoli per la materia. Vale la pena considerare se si desidera creare un codice ad alte prestazioni e considerare l'aggiunta di una funzione virtuale che verrebbe chiamata a frequenza allarmante.
Tuttavia, anche tieni presente che sostituire le chiamate di funzioni virtuali con altri mezzi di ramificazione ( if .. else
, switch
, puntatori di funzione, ecc.) Non risolverà il problema fondamentale - potrebbe beh, è più lento Il problema (se esiste) non è funzioni virtuali ma (non necessario) indiretto.
Modifica: la differenza nelle istruzioni di chiamata è descritta in altre risposte. Fondamentalmente, il codice per una chiamata statica ("normale") è:
- Copia alcuni registri in pila, per consentire alla funzione chiamata di utilizzare quei registri.
- Copia gli argomenti in posizioni predefinite, in modo che la funzione chiamata possa trovarli indipendentemente da dove viene chiamata.
- Inserisci l'indirizzo di ritorno.
- Ramo / salta al codice della funzione, che è un indirizzo in fase di compilazione e quindi codificato nel file binario dal compilatore / linker.
- Ottieni il valore di ritorno da una posizione predefinita e ripristina i registri che vogliamo utilizzare.
Una chiamata virtuale fa esattamente la stessa cosa, tranne che l'indirizzo della funzione non è noto al momento della compilazione. Invece, un paio di istruzioni ...
- Ottieni il puntatore vtable, che punta a una serie di puntatori di funzione (indirizzi di funzione), uno per ogni funzione virtuale, dall'oggetto.
- Ottieni l'indirizzo di funzione corretto dal vtable in un registro (l'indice in cui è archiviato l'indirizzo corretto della funzione viene deciso in fase di compilazione).
- Vai all'indirizzo in quel registro, piuttosto che saltare a un indirizzo codificato.
Come per i rami: un ramo è tutto ciò che salta a un'altra istruzione invece di lasciare che l'istruzione successiva venga eseguita. Questo include if
, switch
, parti di vari cicli, chiamate di funzione, ecc. Ea volte il compilatore implementa cose che non sembrano diramarsi in un modo che ha effettivamente bisogno di un ramo sotto il cofano. Vedi Perché l'elaborazione di un array ordinato è più veloce di un array non ordinato? per il motivo per cui questo potrebbe essere lento, ciò che le CPU fanno per contrastare questo rallentamento, e come questo non è un toccasana.