Domanda

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. Giusto?

È stato utile?

Soluzione

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. Luoghi migliori in cui cercare miglioramenti delle prestazioni sono negli algoritmi e negli I / O.

Un eccellente articolo che parla di funzioni virtuali (e altro) è Puntatori di funzioni membro e Delegati C ++ più veloci possibili .

Altri suggerimenti

La tua domanda mi ha incuriosito, quindi sono andato avanti e ho eseguito alcuni cronometri sulla CPU PowerPC 3G in ordine 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 (A.x = B.x + C.x) 1000 volte. Ho eseguito questo con le funzioni definite come inline , virtual e 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 ha causato esattamente chiamate di funzione 3 * 4 * 1024 = 12.288 (1024 vettori volte quattro componenti per tre chiamate per aggiunta), quindi questi tempi rappresentano 1000 * 12.288 = 12.288.000 chiamate 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.

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

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 è 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.

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

  

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 ...

assolutamente. Un problema risale a quando i computer funzionavano a 100 Mhz, poiché ogni chiamata di metodo richiedeva una ricerca sulla vtable prima che venisse 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 è stato usato molto e creato un'istanza nel codice basato su modelli che è finito 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.

Esistono altri criteri di prestazione oltre al tempo di esecuzione. Una Vtable occupa anche spazio di memoria e in alcuni casi può essere evitata: ATL utilizza il tempo di compilazione " associazione dinamica simulata " con modelli per ottenere l'effetto di "polimorfismo statico", che è un po 'difficile da spiegare ; fondamentalmente passi la classe derivata come parametro a un modello di classe base, quindi in fase di compilazione la classe base "sa" 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 lo farà mai verificarsi.

Vedi anche questa altra domanda SO .

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

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

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 altro "pesante" operazione di memoria da eseguire.

Anche se come altre persone hanno detto, non sarà più 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" il tuo codice per un vantaggio in termini di prestazioni.

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.

Sono andato avanti e indietro su questo almeno 20 volte sul mio particolare progetto. Sebbene possa avere grandi vantaggi in termini di riutilizzo del codice, chiarezza, manutenibilità e leggibilità, d'altra parte, i risultati delle prestazioni ancora esistono esistono 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 aiutare a rendere il tuo codice il più utilizzabile possibile.

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.

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 " maggio " perché questo dipende dal compilatore, dalla cache ecc.

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 quanto ti serve.

Mi sono sempre posto delle domande, soprattutto perché - alcuni anni fa - ho anche fatto un test del genere confrontando i tempi di una chiamata di un metodo membro standard con uno virtuale e in quel momento ero davvero arrabbiato per i risultati, avendo vuoto le chiamate virtuali sono 8 volte più lente delle 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 fatto di nuovo 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 contasse più nulla. Sebbene abbia senso avere inline più veloci dei non virtuali e che siano più veloci dei virtuali, spesso dipende dal carico del computer in generale, 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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top