Funzioni virtuali e prestazioni - C ++


125

Nel mio progetto di classe, utilizzo ampiamente classi astratte e funzioni virtuali. Ho avuto la sensazione che le funzioni virtuali influenzino le prestazioni. È vero? Ma penso che questa differenza di prestazioni non sia evidente e sembra che stia facendo un'ottimizzazione prematura. Destra?


Secondo la mia risposta, suggerisco di chiuderlo come duplicato di stackoverflow.com/questions/113830
Suma,


2
Se stai eseguendo calcoli ad alte prestazioni e crunching dei numeri, non utilizzare alcuna virtualità nel nucleo del calcolo: sicuramente uccide tutte le prestazioni e impedisce le ottimizzazioni in fase di compilazione. Per l'inizializzazione o la finalizzazione del programma non è importante. Quando si lavora con le interfacce, è possibile utilizzare la virtualità come desiderato.
Vincent,

Risposte:


90

Una buona regola empirica è:

Non è un problema di prestazioni fino a quando non puoi dimostrarlo.

L'uso di funzioni virtuali avrà un leggero effetto sulle prestazioni, ma è improbabile che influisca sulle prestazioni complessive dell'applicazione. I posti migliori in cui cercare miglioramenti delle prestazioni sono negli algoritmi e negli I / O.

Un eccellente articolo che parla di funzioni virtuali (e altro) sono i puntatori di funzioni membro e i delegati C ++ più veloci possibili .


Che dire delle pure funzioni virtuali? Interessano le prestazioni in qualche modo? Mi chiedo solo come sembra che siano lì semplicemente per imporre l'implementazione.
Thom

2
@thomthom: Corretto, non vi è alcuna differenza di prestazioni tra funzioni virtuali normali e virtuali normali.
Greg Hewgill,

168

La tua domanda mi ha incuriosito, quindi sono andato avanti e ho eseguito alcuni cronometri sulla CPU PowerPC 3GHz con cui lavoriamo. Il test che ho eseguito è stato quello di creare una semplice classe vettoriale 4d con funzioni get / set

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

Quindi ho impostato tre array contenenti ciascuno 1024 di questi vettori (abbastanza piccoli da adattarsi a L1) e ho eseguito un loop che li ha aggiunti l'uno all'altro (Ax = Bx + Cx) 1000 volte. Ho eseguito questo con le funzioni definite come inline, virtuale chiamate di funzione regolari. Ecco i risultati:

  • in linea: 8 ms (0,65 ns per chiamata)
  • diretto: 68 ms (5,53 ns per chiamata)
  • virtuale: 160 ms (13 ns per chiamata)

Quindi, in questo caso (dove tutto si inserisce nella cache) le chiamate di funzione virtuale erano circa 20 volte più lente delle chiamate in linea. Ma cosa significa veramente? Ogni viaggio attraverso il loop causava esattamente 3 * 4 * 1024 = 12,288chiamate di funzione (1024 vettori volte quattro componenti per tre chiamate per aggiunta), quindi questi tempi rappresentano 1000 * 12,288 = 12,288,000chiamate di funzione. Il loop virtuale ha impiegato 92 ms in più rispetto al loop diretto, quindi l'overhead aggiuntivo per chiamata era di 7 nanosecondi per funzione.

Da ciò concludo: , le funzioni virtuali sono molto più lente delle funzioni dirette, e no , a meno che tu non abbia intenzione di chiamarle dieci milioni di volte al secondo, non importa.

Vedi anche: confronto dell'assieme generato.


Ma se vengono chiamati più volte, spesso possono essere più economici rispetto a quando vengono chiamati una sola volta. Vedi il mio blog irrilevante: phresnel.org/blog , i post dal titolo "Funzioni virtuali considerate non dannose", ma ovviamente dipende dalla complessità dei tuoi codepati
Sebastian Mach,

22
Il mio test misura un piccolo set di funzioni virtuali chiamate ripetutamente. Il tuo post sul blog presuppone che il costo in termini di tempo del codice possa essere misurato contando le operazioni, ma ciò non è sempre vero; il costo maggiore di un vfunc sui processori moderni è la bolla della pipeline causata da un errore di filiale.
Crashworks,

10
questo sarebbe un ottimo punto di riferimento per gcc LTO (Link Time Optimization); prova a compilarlo di nuovo con lto abilitato: gcc.gnu.org/wiki/LinkTimeOptimization e guarda cosa succede con il fattore 20x
lurscher,

1
Se una classe ha una funzione virtuale e una inline, anche le prestazioni del metodo non virtuale saranno influenzate? Semplicemente per natura della classe che è virtuale?
Thom

4
@thomthom No, virtuale / non virtuale è un attributo per funzione. Una funzione deve essere definita tramite vtable solo se è contrassegnata come virtuale o se ha la precedenza su una classe base che la ha come virtuale. Vedrai spesso classi che hanno un gruppo di funzioni virtuali per l'interfaccia pubblica, quindi molti accessori in linea e così via. (Tecnicamente, questo è specifico per l'implementazione e un compilatore potrebbe usare ponter virtuali anche per funzioni contrassegnate come "inline", ma una persona che ha scritto un tale compilatore sarebbe pazza.)
Crashworks,

42

Quando Objective-C (dove tutti i metodi sono virtuali) è la lingua principale per iPhone e Java stranamente è la lingua principale per Android, penso che sia abbastanza sicuro usare le funzioni virtuali C ++ sulle nostre torri dual-core a 3 GHz.


4
Non sono sicuro che l'iPhone sia un buon esempio di codice performante: youtube.com/watch?v=Pdk2cJpSXLg
Crashworks

13
@Crashworks: l'iPhone non è affatto un esempio di codice. È un esempio di hardware, in particolare hardware lento , che è il punto che stavo facendo qui. Se questi linguaggi presumibilmente "lenti" sono abbastanza buoni per hardware sottodimensionato, le funzioni virtuali non saranno un grosso problema.
Chuck,

52
L'iPhone funziona con un processore ARM. I processori ARM utilizzati per iOS sono progettati per un basso consumo di MHz e basso consumo. Non esiste silicio per la previsione di diramazione sulla CPU e quindi non ci sono costi di gestione dovuti a mancate previsioni di diramazione da chiamate di funzione virtuale. Anche l'hardware MHz per iOS è abbastanza basso da impedire a una cache cache di arrestare il processore per 300 cicli di clock mentre recupera i dati dalla RAM. I cache miss sono meno importanti a MHz inferiore. In breve, non ci sono costi di gestione delle funzioni virtuali sui dispositivi iOS, ma si tratta di un problema hardware che non si applica alle CPU desktop.
HaltingState,

4
Come programmatore Java di lunga data di recente in C ++, voglio aggiungere che il compilatore JIT Java e l'ottimizzatore di runtime ha la capacità di compilare, prevedere e persino incorporare alcune funzioni in fase di esecuzione dopo un numero predefinito di loop. Tuttavia, non sono sicuro che C ++ abbia tale funzionalità al momento della compilazione e del collegamento perché manca il modello di chiamata di runtime. Quindi in C ++ potremmo aver bisogno di stare leggermente più attenti.
Alex Suo,

@AlexSuo Non sono sicuro del tuo punto? Essendo compilato, il C ++ ovviamente non può ottimizzare in base a ciò che potrebbe accadere in fase di esecuzione, quindi la previsione ecc. Dovrebbe essere fatta dalla CPU stessa ... ma i buoni compilatori C ++ (se richiesti) fanno di tutto per ottimizzare funzioni e loop molto prima runtime.
underscore_d

34

In applicazioni molto critiche per le prestazioni (come i videogiochi) una chiamata di funzione virtuale può essere troppo lenta. Con l'hardware moderno, la principale preoccupazione per le prestazioni è la mancanza della cache. Se i dati non sono nella cache, potrebbero essere centinaia di cicli prima che siano disponibili.

Una normale chiamata di funzione può generare un errore nella cache delle istruzioni quando la CPU recupera la prima istruzione della nuova funzione e non si trova nella cache.

Una chiamata di funzione virtuale deve prima caricare il puntatore vtable dall'oggetto. Ciò può comportare una mancata cache dei dati. Quindi carica il puntatore a funzione dalla vtable, il che può comportare la perdita di un'altra cache di dati. Quindi chiama la funzione che può provocare la mancanza di una cache delle istruzioni come una funzione non virtuale.

In molti casi, due mancati errori nella cache non sono un problema, ma in un ciclo stretto sul codice critico per le prestazioni può ridurre drasticamente le prestazioni.


6
Giusto, ma qualsiasi codice (o vtable) che viene chiamato ripetutamente da un loop stretto (ovviamente) raramente soffrirà di mancate cache. Inoltre, il puntatore vtable si trova in genere nella stessa riga della cache degli altri dati nell'oggetto a cui accederà il metodo chiamato, quindi spesso stiamo parlando di una sola mancanza di cache aggiuntiva.
Qwertie,

5
@Qwertie Non penso sia necessario vero. Il corpo del ciclo (se più grande della cache L1) potrebbe "ritirare" il puntatore vtable, il puntatore alla funzione e la successiva iterazione dovrebbe attendere l'accesso alla cache L2 (o più) su ogni iterazione
Ghita

30

Dalla pagina 44 del manuale "Ottimizzazione del software in C ++" di Agner Fog :

Il tempo necessario per chiamare una funzione di membro virtuale è di alcuni cicli di clock più di quanto ci vuole per chiamare una funzione di membro non virtuale, a condizione che l'istruzione di chiamata di funzione chiami sempre la stessa versione della funzione virtuale. Se la versione cambia, si riceverà una penalità di errore di 10-30 cicli di clock. Le regole per la previsione e l'errata previsione delle chiamate alle funzioni virtuali sono le stesse delle istruzioni switch ...


Grazie per questo riferimento. I manuali di ottimizzazione di Agner Fog sono lo standard di riferimento per un utilizzo ottimale dell'hardware.
Arto Bendiken,

Sulla base del mio ricordo e di una rapida ricerca - stackoverflow.com/questions/17061967/c-switch-and-jump-tables - dubito che sia sempre vero switch. Con casevalori totalmente arbitrari , certo. Ma se tutti gli cases sono consecutivi, un compilatore potrebbe essere in grado di ottimizzarlo in una tabella di salto (ah, che mi ricorda i bei vecchi tempi Z80), che dovrebbe essere (per mancanza di un termine migliore) a tempo costante. Non che consiglio di provare a sostituire vfuncs switch, il che è ridicolo. ;)
underscore_d

7

assolutamente. Era un problema quando i computer funzionavano a 100 Mhz, poiché ogni chiamata di metodo richiedeva una ricerca sulla vtable prima che fosse chiamata. Ma oggi .. su una CPU 3Ghz che ha cache di 1 ° livello con più memoria rispetto al mio primo computer? Affatto. Allocare memoria dalla RAM principale ti costerà più tempo che se tutte le tue funzioni fossero virtuali.

È come ai vecchi tempi in cui le persone dicevano che la programmazione strutturata era lenta perché tutto il codice era suddiviso in funzioni, ogni funzione richiedeva allocazioni di stack e una chiamata di funzione!

L'unica volta in cui mi viene in mente di preoccuparmi di considerare l'impatto sulle prestazioni di una funzione virtuale, è se è stata utilizzata molto intensamente e creata un'istanza nel codice basato su modelli che è finita in tutto. Anche allora, non ci dedicherei troppo!

PS pensa ad altri linguaggi 'facili da usare': tutti i loro metodi sono virtuali sotto le coperte e al giorno d'oggi non strisciano.


4
Bene, anche oggi evitare le chiamate di funzione è importante per le app di alta qualità. La differenza è che i compilatori di oggi incorporano in modo affidabile piccole funzioni in modo da non subire penali di velocità per la scrittura di piccole funzioni. Per quanto riguarda le funzioni virtuali, le CPU intelligenti possono eseguire la previsione del ramo intelligente su di esse. Il fatto che i vecchi computer fossero più lenti, penso, non è proprio il problema - sì, erano molto più lenti, ma allora lo sapevamo, quindi abbiamo dato loro carichi di lavoro molto più piccoli. Nel 1992, se avessimo suonato un MP3, sapevamo che avremmo dovuto dedicare più della metà della CPU a tale compito.
Qwertie,

6
Le date mp3 risalgono al 1995. Nel 92 avevamo a malapena 386, in nessun modo potevano riprodurre un mp3, e il 50% del tempo della cpu presuppone un buon sistema operativo multi-task, un processo inattivo e un programmatore preventivo. Nulla di tutto ciò esisteva sul mercato di consumo al momento. era al 100% dal momento in cui il potere era acceso, fine della storia.
v.oddou,

7

Esistono altri criteri prestazionali oltre ai tempi di esecuzione. Una Vtable occupa anche spazio di memoria, e in alcuni casi può essere evitata: ATL utilizza " associazione dinamica simulata " in fase di compilazione con modelliottenere l'effetto del "polimorfismo statico", che è difficile da spiegare; fondamentalmente si passa la classe derivata come parametro a un modello di classe base, quindi al momento della compilazione la classe base "conosce" quale sia la sua classe derivata in ogni istanza. Non ti consente di memorizzare più classi derivate diverse in una raccolta di tipi di base (che è polimorfismo di runtime) ma da un senso statico, se vuoi creare una classe Y che è uguale a un modello preesistente classe X che ha il ganci per questo tipo di override, devi solo sovrascrivere i metodi che ti interessano, e quindi ottieni i metodi di base di classe X senza dover avere una vtable.

Nelle classi con ingombri di memoria elevati, il costo di un singolo puntatore vtable non è molto, ma alcune delle classi ATL in COM sono molto piccole e vale la pena risparmiare vtable se il caso del polimorfismo di runtime non si verificherà mai.

Vedi anche questa altra domanda SO .

A proposito ecco un post che ho scoperto che parla degli aspetti prestazionali della CPU.



4

Sì, hai ragione e se sei curioso del costo della chiamata alla funzione virtuale potresti trovare questo post interessante.


1
L'articolo collegato non considera la parte molto importante della chiamata virtuale e questo è possibile errore di filiale.
Suma,

4

L'unico modo in cui riesco a vedere che una funzione virtuale diventerà un problema di prestazioni è se molte funzioni virtuali vengono chiamate in un ciclo stretto e se e solo se causano un errore di pagina o altre operazioni di memoria "pesanti".

Anche se come altre persone hanno detto che non sarà mai un problema per te nella vita reale. E se lo pensi, esegui un profiler, fai alcuni test e verifica se questo è davvero un problema prima di provare a "annullare la progettazione" del tuo codice per un vantaggio in termini di prestazioni.


2
chiamare qualsiasi cosa in un circuito stretto probabilmente manterrà tutto quel codice e dati caldi nella cache ...
Greg Rogers

2
Sì, ma se quel ciclo a destra scorre attraverso un elenco di oggetti, ogni oggetto potrebbe potenzialmente chiamare una funzione virtuale a un indirizzo diverso tramite la stessa chiamata di funzione.
Daemin,

3

Quando il metodo class non è virtuale, il compilatore di solito fa in-lining. Al contrario, quando si utilizza il puntatore a una classe con funzione virtuale, l'indirizzo reale sarà noto solo in fase di esecuzione.

Questo è ben illustrato dal test, differenza di tempo ~ 700% (!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

L'impatto della chiamata di funzione virtuale dipende fortemente dalla situazione. Se ci sono poche chiamate e una notevole quantità di lavoro all'interno della funzione, potrebbe essere trascurabile.

Oppure, quando si tratta di una chiamata virtuale utilizzata più volte più volte, durante una semplice operazione, potrebbe essere davvero grande.


4
Una chiamata di funzione virtuale è costosa rispetto a ++ia. E allora?
Bo Persson,

2

Sono andato avanti e indietro su questo almeno 20 volte sul mio particolare progetto. Anche se ci possono essere alcuni grandi guadagni in termini di riutilizzo del codice, la chiarezza, la manutenibilità, e la leggibilità, d'altra parte, colpi alle prestazioni ancora fanno esistere con funzioni virtuali.

Il successo prestazionale sarà evidente su un moderno laptop / desktop / tablet ... probabilmente no! Tuttavia, in alcuni casi con sistemi incorporati, l'hit prestazioni potrebbe essere il fattore trainante dell'inefficienza del codice, soprattutto se la funzione virtuale viene richiamata più volte in un ciclo.

Ecco un documento datato che analizza le migliori pratiche per C / C ++ nel contesto dei sistemi integrati: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

Per concludere: spetta al programmatore comprendere i pro / contro dell'uso di un determinato costrutto rispetto a un altro. A meno che tu non sia guidato dalle prestazioni super, probabilmente non ti interessa il successo delle prestazioni e dovresti usare tutte le cose OO ordinate in C ++ per rendere il tuo codice il più utilizzabile possibile.


2

Nella mia esperienza, la cosa principale rilevante è la capacità di incorporare una funzione. Se hai esigenze di prestazioni / ottimizzazione che determinano la necessità di incorporare una funzione, non puoi renderla virtuale perché ciò lo impedirebbe. Altrimenti, probabilmente non noterai la differenza.


1

Una cosa da notare è che questo:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

potrebbe essere più veloce di questo:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

Questo perché il primo metodo chiama solo una funzione mentre il secondo può chiamare molte funzioni diverse. Questo vale per qualsiasi funzione virtuale in qualsiasi lingua.

Dico "may" perché questo dipende dal compilatore, dalla cache ecc.


0

La penalità prestazionale dell'utilizzo delle funzioni virtuali non può mai superare i vantaggi che si ottengono a livello di progettazione. Presumibilmente una chiamata a una funzione virtuale sarebbe del 25% meno efficiente di una chiamata diretta a una funzione statica. Questo perché esiste un livello di riferimento indiretto attraverso VMT. Tuttavia, il tempo impiegato per effettuare la chiamata è normalmente molto ridotto rispetto al tempo impiegato nell'esecuzione effettiva della funzione, pertanto il costo totale delle prestazioni sarà irrilevante, soprattutto con le prestazioni attuali dell'hardware. Inoltre, il compilatore può talvolta ottimizzare e vedere che non è necessaria alcuna chiamata virtuale e compilarlo in una chiamata statica. Quindi non preoccuparti, usa le funzioni virtuali e le classi astratte di cui hai bisogno.


2
mai e poi mai, non importa quanto piccolo sia il computer di destinazione?
zumalifeguard,

Avrei potuto essere d'accordo se l'avessi detto come The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.la differenza chiave sta dicendo sometimes, no never.
underscore_d

-1

Mi sono sempre posto delle domande, soprattutto perché - alcuni anni fa - ho anche fatto un test simile confrontando i tempi di una chiamata di metodo membro standard con una virtuale e in quel momento ero davvero arrabbiato per i risultati, avendo chiamate virtuali vuote essendo 8 volte più lento dei non virtuali.

Oggi ho dovuto decidere se utilizzare o meno una funzione virtuale per allocare più memoria nella mia classe di buffer, in un'app molto critica per le prestazioni, quindi ho cercato su Google (e ti ho trovato), e alla fine ho ripetuto il test.

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

Ed è stato davvero sorpreso dal fatto che, in effetti, non contenga più nulla. Sebbene abbia senso avere inline più veloci dei non virtuali e che siano più veloci dei virtuali, spesso si tratta del carico complessivo del computer, indipendentemente dal fatto che la cache disponga o meno dei dati necessari e che sia possibile ottimizzare a livello di cache, penso, che ciò dovrebbe essere fatto dagli sviluppatori del compilatore più che dagli sviluppatori di applicazioni.


12
Penso che sia abbastanza probabile che il tuo compilatore possa dire che la chiamata di funzione virtuale nel tuo codice può chiamare solo Virtual :: call. In tal caso, può semplicemente incorporarlo. Inoltre, nulla impedisce al compilatore di incorporare Normal :: call anche se non è stato richiesto. Quindi penso che sia possibile che tu ottenga gli stessi tempi per le 3 operazioni perché il compilatore sta generando un codice identico per loro.
Bjarke H. Roune,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.