2013-01-14 17 views
5

Ecco il mio problema. Ho una classe base e una classe derivata che sovrascrive alcuni metodi dalla classe base. Per semplicità si consideri il seguente esempio:Si può evitare un sovraccarico vtable usando un static_cast?

struct base 
{ 
    virtual void fn() 
    {/*base definition here*/} 
}; 

struct derived : base 
{ 
    void fn() 
    {/*derived definition here*/} 
}; 

Nel mio programma vero e proprio, queste classi sono passate come argomenti di altre classi e sono chiamati in altri metodi, ma per ragioni di semplicità creiamo una semplice funzione che prende come argomento o la classe base o derivata. Posso semplicemente scrivere

void call_fn(base& obj) 
{obj.fn();} 

e la chiamata alla funzione appropriata sarà risolto a run-time a causa delle funzioni virtuali.

Sono preoccupato, tuttavia, che se il call_fn, deve essere chiamato milioni di volte (che nel mio caso sarà come la mia applicazione reale è un esperimento di simulazione), otterrò un overhead significativo che vorrei evitare .

Quindi, mi chiedevo se l'utilizzo di un static_cast potrebbe effettivamente affrontare il problema. Forse qualcosa di simile:

template <typename T> 
void call_fn(base& obj) 
{(static_cast<T*>(&obj))->fn();} 

In questo caso, la chiamata di funzione sarebbe fatto come call_fn<base>(obj) per chiamare il metodo base o call_fn<derived>(obj) per chiamare il metodo derivato.

Questa soluzione eviterà l'overhead vtable o sarà comunque interessata? Grazie in anticipo per eventuali risposte!

A proposito, sono a conoscenza del CRTP ma non lo conosco molto bene. Questo è il motivo per cui vorrei conoscere prima la risposta a questa semplice domanda :)

+2

Qual è la definizione di * overhead significativo *? Potresti essere sorpreso di ciò che l'overhead di chiamare una funzione virtuale è qualche milione di volte. –

+1

Hai effettivamente dimostrato che l'overhead vtable è un problema con il tuo codice? E se è così, puoi dimostrare che facendo il relativo "se (è questa classe) fai questo altrimenti" è più veloce? Sospetto che non lo sia, a meno che non faccia in modo che il compilatore sia in linea con la funzione e questo fa risparmiare un sacco di sforzi. –

+0

Non l'ho provato ... è per questo che ho voluto creare un metodo che eviti il ​​vtable, in modo da poter confrontare i due e avere un'idea più chiara di quanto sia significativo il sovraccarico :) – linuxfever

risposta

6

Questa soluzione eviterà l'overhead vtable o sarà comunque interessata?

Continuerà a utilizzare la spedizione dinamica (indipendentemente dal fatto che ciò causi un notevole overhead è una questione completamente diversa). È possibile disattivare la spedizione dinamica qualificando la chiamata di funzione come in:

static_cast<T&>(obj).T::fn(); 

Anche se non vorrei anche provare a farlo. Lascia la spedizione dinamica, quindi verifica le prestazioni dell'applicazione, esegui qualche profilazione, fai ulteriori profilature. Rivedi di nuovo il profilo per assicurarti di capire cosa ti sta dicendo il profiler. Solo allora, considera di apportare una modifica e un profilo di nuovo per verificare se le tue supposizioni sono corrette o meno.

+0

+1 questa è la risposta. Questa è una tecnica valida SE sei completamente sicuro che troppa CPU è una piccola funzione virtuale in un enorme loop e non puoi correggerla correttamente. Trasforma 'for (...) {(vtbl-> foo)(); } 'in' if (a) {for (...) A :: foo(); } else {for (...) B :: foo(); } '. Non è probabile che faccia molta differenza. – doug65536

+0

@linuxfever Se non si tratta di un ciclo continuo attorno a una riga o più di una funzione virtuale, la chiamata in overlay di vtbl sarà trascurabile. La cosa bella dei loop stretti è che la previsione dei branch funziona su chiamate indirette a CPU moderne (meglio di circa AMD K6 del 1997). Supponendo che il tuo compilatore non stia generando troppo codice lavorando con il passaggio degli argomenti, sarà veloce. C++ usa vtbls perché sono il modo più veloce per chiamare qualcosa di sconosciuto. – doug65536

+0

@linuxfever D'altra parte, il compilatore potrebbe essere in grado di sollevare una tonnellata di sottoespressioni comuni dal ciclo se inline la chiamata diretta (non virtuale). – doug65536

0

Il VTable risiede nella classe. Se si dispone di membri virtuali, sarà possibile accedere tramite VTable. Il cast non influirà sulla presenza o meno del VTable, né sul modo in cui i membri sono accessibili.

+0

Non vero. Se il compilatore conosce il tipo statico della variabile in fase di esecuzione, può chiamare direttamente la funzione anziché passare attraverso il vtable. Le funzioni virtuali vengono utilizzate solo con puntatori e riferimenti. –

+0

@MarkRansom Tutte le chiamate alle funzioni virtuali passano attraverso il vtbl, non importa se il compilatore "conosce" il tipo. Un ottimizzatore globale potrebbe ottimizzare tale scenario, ma non sarebbe probabile che sia così aggressivo a meno che non stia facendo ottimizzazioni guidate dai profili. La chiave per ottenere prestazioni bypassando le chiamate vtbl è in -ining. A meno che il compilatore non sia sicuro che la maggior parte delle chiamate riguardi un certo tipo, probabilmente farebbe solo una chiamata indiretta. – doug65536

+0

@ doug65536, guarda l'assemblatore generato per una chiamata a una funzione virtuale su una variabile locale (non un puntatore o riferimento) e dimmi cosa vedi. Sono d'accordo che evitando il vtable probabilmente non compri molto. –

5

Questa non è davvero una risposta alla tua domanda, ma ero curioso di sapere "quale è veramente l'overhead di chiamare una funzione virtuale contro una normale funzione di classe". Per renderlo "equo", ho creato un classes.cpp che implementa una funzione molto semplice, ma è un file separato che viene compilato al di fuori del "main".

classes.h:

#ifndef CLASSES_H 
#define CLASSES_H 

class base 
{ 
    virtual int vfunc(int x) = 0; 
}; 

class vclass : public base 
{ 
public: 
    int vfunc(int x); 
}; 


class nvclass 
{ 
public: 
    int nvfunc(int x); 
}; 


nvclass *nvfactory(); 
vclass* vfactory(); 


#endif 

classi.cpp:

#include "classes.h" 

int vclass:: vfunc(int x) 
{ 
    return x+1; 
} 


int nvclass::nvfunc(int x) 
{ 
    return x+1; 
} 

nvclass *nvfactory() 
{ 
    return new nvclass; 
} 

vclass* vfactory() 
{ 
    return new vclass; 
} 

Questo è chiamato da:

#include <cstdio> 
#include <cstdlib> 
#include "classes.h" 

#if 0 
#define ASSERT(x) do { if(!(x)) { assert_fail(__FILE__, __LINE__, #x); } } while(0) 
static void assert_fail(const char* file, int line, const char *cond) 
{ 
    fprintf(stderr, "ASSERT failed at %s:%d condition: %s \n", file, line, cond); 
    exit(1); 
} 
#else 
#define ASSERT(x) (void)(x) 
#endif 

#define SIZE 10000000 

static __inline__ unsigned long long rdtsc(void) 
{ 
    unsigned hi, lo; 
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi)); 
    return ((unsigned long long)lo)|(((unsigned long long)hi)<<32); 
} 


void print_avg(const char *str, const int *diff, int size) 
{ 
    int i; 
    long sum = 0; 
    for(i = 0; i < size; i++) 
    { 
    int t = diff[i]; 
    sum += t; 
    } 

    printf("%s average =%f clocks\n", str, (double)sum/size); 
} 


int diff[SIZE]; 

int main() 
{ 
    unsigned long long a, b; 
    int i; 
    int sum = 0; 
    int x; 

    vclass *v = vfactory(); 
    nvclass *nv = nvfactory(); 


    for(i = 0; i < SIZE; i++) 
    { 
    a = rdtsc(); 

    x = 16; 
    sum+=x; 
    b = rdtsc(); 

    diff[i] = (int)(b - a); 
    } 

    print_avg("Emtpy", diff, SIZE); 


    for(i = 0; i < SIZE; i++) 
    { 
    a = rdtsc(); 

    x = 0; 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    x = v->vfunc(x); 
    ASSERT(x == 4); 
    sum+=x; 
    b = rdtsc(); 

    diff[i] = (int)(b - a); 
    } 

    print_avg("Virtual", diff, SIZE); 

    for(i = 0; i < SIZE; i++) 
    { 
    a = rdtsc(); 
    x = 0; 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    x = nv->nvfunc(x); 
    ASSERT(x == 4);  
    sum+=x; 
    b = rdtsc(); 
    diff[i] = (int)(b - a); 
    } 
    print_avg("no virtual", diff, SIZE); 

    printf("sum=%d\n", sum); 

    delete v; 
    delete nv; 

    return 0; 
} 

La vera differenza nel codice è questo: chiamata virtuale:

40066b: ff 10     callq *(%rax) 

chiamata non virtuale:

4006d3: e8 78 01 00 00   callq 400850 <_ZN7nvclass6nvfuncEi> 

E i risultati:

Emtpy average =78.686081 clocks 
Virtual average =144.732567 clocks 
no virtual average =122.781466 clocks 
sum=480000000 

Ricordiamo che questo è il sovraccarico per 16 chiamate per loop, quindi la differenza tra chiamare una funzione e non chiamare una funzione è di circa 5 cicli di clock per iterazione [compresi sommando i risultati e l'altra elaborazione richiesto], e la chiamata virtuale aggiunge 22 clock per iterazione, quindi circa 1,5 orologi per chiamata.

Dubito che si noterà, assumendo che si faccia qualcosa di un po 'più significativo del ritorno x + 1 nella propria funzione.

+0

+1 Bella analisi, anche se dovresti includere anche la ricerca nella tua "vera differenza". Se le due linee che hai mostrato fossero effettivamente l'intera differenza, l'implementazione virtuale non sarebbe più lenta! – us2012

+0

+1, nota anche che nel codice reale, la funzione probabilmente fa più di una singola aggiunta e la differenza nella chiamata sarà molto più bassa del costo della funzione. –

+0

La differenza tra le due linee è che si utilizza un metodo indiretto per indirizzare '* (% rax)' e l'altro usa un indirizzo diretto '400850'. Se viene chiamato più raramente che in un circuito chiuso, allora c'è ancora un po 'di lavoro per trovare il vtable e così via, ma poi hai anche altro codice da preoccuparsi del suo effetto sulle chiamate. –

0

Se si dispone di un array polimorfico, in cui gli elementi sono polimorfici ma tutti gli elementi hanno lo stesso tipo, è anche possibile esternalizzare il vtable. Ciò consente di cercare la funzione una volta e quindi chiamarla direttamente su ciascun elemento. In tal caso, C++ non ti aiuta però, dovrai farlo manualmente.

Questo è utile anche se si stanno microptimizzando le cose. Credo che la funzione di Boost usi una tecnica simile. Ha bisogno solo di due funzioni (call e release reference) nel vtable, ma quella generata dal compilatore conterrà anche RTTI e alcune altre cose, che possono essere evitate codificando a mano un vtable che ha solo quei due puntatori di funzione.

Problemi correlati