Quanto è costoso RTTI?

Capisco che ci sia una risorsa colpita dall’uso di RTTI, ma quanto è grande? Ovunque appaia, dico che “RTTI è costoso”, ma nessuno di questi fornisce alcun benchmark o dati quantitativi che reggano la memoria, il tempo del processore o la velocità.

Quindi, quanto è costoso RTTI? Potrei usarlo su un sistema embedded dove ho solo 4MB di RAM, quindi ogni bit conta.

Modifica: Come per la risposta di S. Lott , sarebbe meglio se includessi ciò che sto effettivamente facendo. Sto usando una class per trasmettere dati di diverse lunghezze e che possono eseguire diverse azioni , quindi sarebbe difficile farlo utilizzando solo funzioni virtuali. Sembra che l’utilizzo di alcuni dynamic_cast s possa risolvere questo problema consentendo che le diverse classi derivate vengano passate attraverso i diversi livelli e tuttavia consentano loro di agire in modo completamente diverso.

Dalla mia comprensione, dynamic_cast utilizza RTTI, quindi mi chiedevo quanto sarebbe fattibile utilizzarlo su un sistema limitato.

    Indipendentemente dal compilatore, puoi sempre risparmiare su runtime se puoi permetterti di farlo

     if (typeid(a) == typeid(b)) { B* ba = static_cast(&a); etc; } 

    invece di

     B* ba = dynamic_cast(&a); if (ba) { etc; } 

    Il primo implica solo un confronto di std::type_info ; il secondo implica necessariamente attraversare un albero di ereditarietà e confronti.

    Passato che … come tutti dicono, l’utilizzo delle risorse è specifico per l’implementazione.

    Sono d’accordo con i commenti di tutti gli altri che il mittente dovrebbe evitare RTTI per ragioni di design. Tuttavia, ci sono buone ragioni per usare RTTI (principalmente a causa di boost :: any). Tenendo presente ciò, è utile conoscere l’utilizzo effettivo delle risorse nelle implementazioni comuni.

    Recentemente ho fatto una serie di ricerche su RTTI in GCC.

    tl; dr: RTTI in GCC utilizza uno spazio trascurabile e typeid(a) == typeid(b) è molto veloce, su molte piattaforms (Linux, BSD e forse piattaforms embedded, ma non mingw32). Se sai che sarai sempre su una piattaforma benedetta, RTTI è molto vicino alla libera.

    Dettagli grintosi:

    GCC preferisce utilizzare un particolare C + ABI [1] “vendor-neutral” e usa sempre questo ABI per obiettivi Linux e BSD [2]. Per piattaforms che supportano questo ABI e anche debole linkage, typeid() restituisce un object coerente e univoco per ogni tipo, anche attraverso i limiti di collegamento dinamico. È ansible eseguire il test &typeid(a) == &typeid(b) , o semplicemente fare affidamento sul fatto che il test portatile typeid(a) == typeid(b) effettivamente confronta solo un puntatore internamente.

    Nell’ABI preferito da GCC, una class vtable contiene sempre un puntatore a una struttura RTTI per tipo, sebbene non possa essere utilizzata. Quindi una typeid() tipo typeid() dovrebbe solo costare quanto qualsiasi altra ricerca vtable (lo stesso che si chiama una funzione membro virtuale), e il supporto RTTI non dovrebbe usare spazio aggiuntivo per ogni object.

    Da quello che riesco a capire, le strutture RTTI usate da GCC (queste sono tutte le sottoclassi di std::type_info ) contengono solo pochi byte per ogni tipo, a parte il nome. Non mi è chiaro se i nomi siano presenti nel codice di uscita anche con -fno-rtti . In entrambi i casi, la modifica della dimensione del file binario compilato dovrebbe riflettere la modifica nell’utilizzo della memoria di runtime.

    Un rapido esperimento (usando GCC 4.4.3 su Ubuntu 10.04 64-bit) mostra che -fno-rtti aumenta effettivamente le dimensioni binarie di un semplice programma di test di poche centinaia di byte. Ciò avviene in modo coerente tra le combinazioni di -g e -O3 . Non sono sicuro del perché le dimensioni aumenterebbero; una possibilità è che il codice STL di GCC si comporti in modo diverso senza RTTI (poiché le eccezioni non funzionano).

    [1] Conosciuto come Itanium C ++ ABI, documentato su http://www.codesourcery.com/public/cxx-abi/abi.html . I nomi sono orribilmente confusi: il nome si riferisce all’architettura di sviluppo originale, sebbene le specifiche ABI funzionino su molte architetture tra cui i686 / x86_64. I commenti nella fonte interna di GCC e nel codice STL si riferiscono a Itanium come il “nuovo” ABI in contrasto con il “vecchio” usato precedentemente. Peggio ancora, il “nuovo” / Itanium ABI si riferisce a tutte le versioni disponibili attraverso -fabi-version ; il “vecchio” ABI era precedente a questa versione. GCC ha adottato l’ABI Itanium / versioned / “new” nella versione 3.0; il “vecchio” ABI è stato utilizzato in 2.95 e precedenti, se sto leggendo i loro changelog correttamente.

    [2] Non sono riuscito a trovare alcuna risorsa elencando std::type_info stabilità dell’object per piattaforma. Per i compilatori a cui ho avuto accesso, ho usato quanto segue: echo "#include " | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES echo "#include " | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES echo "#include " | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES . Questa macro controlla il comportamento operator== per std::type_info in STL di GCC, a partire da GCC 3.0. Ho trovato che mingw32-gcc obbedisce all’ABI di Windows C ++, dove std::type_info oggetti non sono univoci per un tipo attraverso DLL; typeid(a) == typeid(b) chiama strcmp sotto le copertine. Suppongo che su obiettivi embedded a programma singolo come AVR, in cui non vi sia alcun codice da colbind, std::type_info oggetti std::type_info sono sempre stabili.

    Dipende dalla scala delle cose. Per la maggior parte sono solo un paio di controlli e alcune deduzioni ai puntatori. Nella maggior parte delle implementazioni, nella parte superiore di ogni object che ha funzioni virtuali, c’è un puntatore a un vtable che contiene un elenco di puntatori a tutte le implementazioni della funzione virtuale su quella class. Direi che la maggior parte delle implementazioni userebbe questo per memorizzare un altro puntatore alla struttura type_info per la class.

    Ad esempio in pseudo-c ++:

     struct Base { virtual ~Base() {} }; struct Derived { virtual ~Derived() {} }; int main() { Base *d = new Derived(); const char *name = typeid(*d).name(); // C++ way // faked up way (this won't actually work, but gives an idea of what might be happening in some implementations). const vtable *vt = reinterpret_cast(d); type_info *ti = vt->typeinfo; const char *name = ProcessRawName(ti->name); } 

    In generale, il vero argomento contro RTTI è l’impossibilità di dover modificare il codice ovunque ogni volta che si aggiunge una nuova class derivata. Invece di istruzioni di commutazione in tutto il mondo, fatelo in funzioni virtuali. Questo sposta tutto il codice che è diverso tra le classi nelle classi stesse, in modo che una nuova derivazione debba solo sovrascrivere tutte le funzioni virtuali per diventare una class pienamente funzionante. Se hai mai dovuto cercare una grande base di codice per ogni volta che qualcuno controlla il tipo di una class e fa qualcosa di diverso, imparerai presto a stare lontano da quello stile di programmazione.

    Se il tuo compilatore ti consente di distriggersre completamente RTTI, il risparmio finale della dimensione del codice risultante può essere significativo, con uno spazio RAM così piccolo. Il compilatore deve generare una struttura type_info per ogni singola class con una funzione virtuale. Se si distriggers RTTI, tutte queste strutture non devono essere incluse nell’immagine eseguibile.

    Forse queste cifre potrebbero aiutare.

    Stavo facendo un test rapido usando questo:

    • GCC Clock () + XCode’s Profiler.
    • 100.000.000 iterazioni di loop.
    • Intel Xeon dual-core 2 x 2,66 GHz.
    • La class in questione è derivata da una singola class base.
    • typeid (). name () restituisce “N12fastdelegate13FastDelegate1IivEE”

    5 casi sono stati testati:

     1) dynamic_cast< FireType* >( mDelegate ) 2) typeid( *iDelegate ) == typeid( *mDelegate ) 3) typeid( *iDelegate ).name() == typeid( *mDelegate ).name() 4) &typeid( *iDelegate ) == &typeid( *mDelegate ) 5) { fastdelegate::FastDelegateBase *iDelegate; iDelegate = new fastdelegate::FastDelegate1< t1 >; typeid( *iDelegate ) == typeid( *mDelegate ) } 

    5 è solo il mio codice attuale, in quanto avevo bisogno di creare un object di quel tipo prima di verificare se è simile a uno che ho già.

    Senza ottimizzazione

    Per i quali i risultati sono stati (ho fatto una media di alcune corse):

     1) 1,840,000 Ticks (~2 Seconds) - dynamic_cast 2) 870,000 Ticks (~1 Second) - typeid() 3) 890,000 Ticks (~1 Second) - typeid().name() 4) 615,000 Ticks (~1 Second) - &typeid() 5) 14,261,000 Ticks (~23 Seconds) - typeid() with extra variable allocations. 

    Quindi la conclusione sarebbe:

    • Per semplici casi di cast senza ottimizzazione typeid() è più di due volte più veloce di dyncamic_cast .
    • Su una macchina moderna la differenza tra i due è di circa 1 nanosecondo (un milionesimo di millisecondo).

    Con ottimizzazione (-Os)

     1) 1,356,000 Ticks - dynamic_cast 2) 76,000 Ticks - typeid() 3) 76,000 Ticks - typeid().name() 4) 75,000 Ticks - &typeid() 5) 75,000 Ticks - typeid() with extra variable allocations. 

    Quindi la conclusione sarebbe:

    • Per semplici casi di cast con ottimizzazione, typeid() è quasi x20 più veloce di dyncamic_cast .

    Grafico

    inserisci la descrizione dell'immagine qui

    Il codice

    Come richiesto nei commenti, il codice è sotto (un po ‘disordinato, ma funziona). ‘FastDelegate.h’ è disponibile da qui .

     #include  #include "FastDelegate.h" #include "cycle.h" #include "time.h" // Undefine for typeid checks #define CAST class ZoomManager { public: template < class Observer, class t1 > void Subscribe( void *aObj, void (Observer::*func )( t1 a1 ) ) { mDelegate = new fastdelegate::FastDelegate1< t1 >; std::cout << "Subscribe\n"; Fire( true ); } template< class t1 > void Fire( t1 a1 ) { fastdelegate::FastDelegateBase *iDelegate; iDelegate = new fastdelegate::FastDelegate1< t1 >; int t = 0; ticks start = getticks(); clock_t iStart, iEnd; iStart = clock(); typedef fastdelegate::FastDelegate1< t1 > FireType; for ( int i = 0; i < 100000000; i++ ) { #ifdef CAST if ( dynamic_cast< FireType* >( mDelegate ) ) #else // Change this line for comparisons .name() and & comparisons if ( typeid( *iDelegate ) == typeid( *mDelegate ) ) #endif { t++; } else { t--; } } iEnd = clock(); printf("Clock ticks: %i,\n", iEnd - iStart ); std::cout << typeid( *mDelegate ).name()<<"\n"; ticks end = getticks(); double e = elapsed(start, end); std::cout << "Elasped: " << e; } template< class t1, class t2 > void Fire( t1 a1, t2 a2 ) { std::cout << "Fire\n"; } fastdelegate::FastDelegateBase *mDelegate; }; class Scaler { public: Scaler( ZoomManager *aZoomManager ) : mZoomManager( aZoomManager ) { } void Sub() { mZoomManager->Subscribe( this, &Scaler::OnSizeChanged ); } void OnSizeChanged( int X ) { std::cout << "Yey!\n"; } private: ZoomManager *mZoomManager; }; int main(int argc, const char * argv[]) { ZoomManager *iZoomManager = new ZoomManager(); Scaler iScaler( iZoomManager ); iScaler.Sub(); delete iZoomManager; return 0; } 

    Bene, il profiler non mente mai.

    Dal momento che ho una gerarchia abbastanza stabile di 18-20 tipi che non sta cambiando molto, mi sono chiesto se usare semplicemente un membro enum’d semplice avrebbe fatto il trucco ed evitato il presunto costo “alto” di RTTI. Ero scettico se RTTI fosse in realtà più costoso della semplice dichiarazione if che introduce. Ragazzo oh ragazzo, vero?

    Si scopre che RTTI è costoso, molto più costoso di un’istruzione equivalente if o un semplice switch a una variabile primitiva in C ++. Quindi la risposta di S.Lott non è completamente corretta, c’è un costo extra per RTTI, e non è dovuto al fatto di avere una dichiarazione if nel mix. È dovuto al fatto che RTTI è molto costoso.

    Questo test è stato eseguito sul compilatore Apple LLVM 5.0, con le ottimizzazioni predefinite triggerste (impostazioni della modalità di rilascio predefinite).

    Quindi, ho meno di 2 funzioni, ognuna delle quali individua il tipo concreto di un object tramite 1) RTTI o 2) un semplice interruttore. Lo fa 50.000.000 di volte. Senza ulteriori indugi, vi presento i runtime relativi per 50.000.000 di esecuzioni.

    inserisci la descrizione dell'immagine qui

    Esatto, i dynamicCasts hanno dynamicCasts 94% del tempo di esecuzione. Mentre il blocco regularSwitch preso solo il 3,3% .

    Per farla breve: se ti puoi permettere l’energia per colbind un enum ‘d type come ho fatto in seguito, probabilmente lo consiglierei, se hai bisogno di fare RTTI e le prestazioni sono di primaria importanza. Richiede solo l’impostazione del membro una volta (assicurarsi di ottenerlo tramite tutti i costruttori ) e assicurarsi di non scriverlo mai dopo.

    Detto questo, fare questo non dovrebbe rovinare le tue pratiche OOP .. è solo pensato per essere usato quando le informazioni sul tipo semplicemente non sono disponibili e ti ritrovi messo in difficoltà nell’usare RTTI.

     #include  #include  using namespace std; enum AnimalClassTypeTag { TypeAnimal=1, TypeCat=1<<2,TypeBigCat=1<<3,TypeDog=1<<4 } ; struct Animal { int typeTag ;// really AnimalClassTypeTag, but it will complain at the |= if // at the |='s if not int Animal() { typeTag=TypeAnimal; // start just base Animal. // subclass ctors will |= in other types } virtual ~Animal(){}//make it polymorphic too } ; struct Cat : public Animal { Cat(){ typeTag|=TypeCat; //bitwise OR in the type } } ; struct BigCat : public Cat { BigCat(){ typeTag|=TypeBigCat; } } ; struct Dog : public Animal { Dog(){ typeTag|=TypeDog; } } ; typedef unsigned long long ULONGLONG; void dynamicCasts(vector &zoo, ULONGLONG tests) { ULONGLONG animals=0,cats=0,bigcats=0,dogs=0; for( ULONGLONG i = 0 ; i < tests ; i++ ) { for( Animal* an : zoo ) { if( dynamic_cast( an ) ) dogs++; else if( dynamic_cast( an ) ) bigcats++; else if( dynamic_cast( an ) ) cats++; else //if( dynamic_cast( an ) ) animals++; } } printf( "%lld animals, %lld cats, %lld bigcats, %lld dogs\n", animals,cats,bigcats,dogs ) ; } //*NOTE: I changed from switch to if/else if chain void regularSwitch(vector &zoo, ULONGLONG tests) { ULONGLONG animals=0,cats=0,bigcats=0,dogs=0; for( ULONGLONG i = 0 ; i < tests ; i++ ) { for( Animal* an : zoo ) { if( an->typeTag & TypeDog ) dogs++; else if( an->typeTag & TypeBigCat ) bigcats++; else if( an->typeTag & TypeCat ) cats++; else animals++; } } printf( "%lld animals, %lld cats, %lld bigcats, %lld dogs\n", animals,cats,bigcats,dogs ) ; } int main(int argc, const char * argv[]) { vector zoo ; zoo.push_back( new Animal ) ; zoo.push_back( new Cat ) ; zoo.push_back( new BigCat ) ; zoo.push_back( new Dog ) ; ULONGLONG tests=50000000; dynamicCasts( zoo, tests ) ; regularSwitch( zoo, tests ) ; } 

    Il modo standard:

     cout << (typeid(Base) == typeid(Derived)) << endl; 

    Lo standard RTTI è costoso perché si basa sul confronto di una stringa sottostante e quindi la velocità di RTTI può variare in base alla lunghezza del nome della class.

    Il motivo per cui vengono utilizzati i confronti delle stringhe è di farlo funzionare in modo coerente tra i limiti della libreria / DLL. Se costruisci la tua applicazione staticamente e / o stai usando alcuni compilatori, puoi probabilmente usare:

     cout << (typeid(Base).name() == typeid(Derived).name()) << endl; 

    Quale non è garantito per funzionare (non darà mai un falso positivo, ma può dare falsi negativi) ma può essere fino a 15 volte più veloce. Questo dipende dall'implementazione di typeid () per funzionare in un certo modo e tutto ciò che si sta facendo è confrontare un puntatore char interno. Anche questo a volte equivale a:

     cout << (&typeid(Base) == &typeid(Derived)) << endl; 

    Puoi comunque utilizzare un ibrido in modo sicuro, che sarà molto veloce se i tipi corrispondono, e sarà il caso peggiore per i tipi non abbinati:

     cout << ( typeid(Base).name() == typeid(Derived).name() || typeid(Base) == typeid(Derived) ) << endl; 

    Per capire se è necessario ottimizzare questo è necessario vedere quanto tempo spendi per ottenere un nuovo pacchetto, rispetto al tempo necessario per elaborare il pacchetto. Nella maggior parte dei casi un confronto di stringhe probabilmente non sarà un big overhead. (dipende dalla tua class o spazio dei nomi :: lunghezza del nome della class)

    Il modo più sicuro per ottimizzare questo è implementare il proprio typeid come un int (o un enum Type: int) come parte della class Base e usarlo per determinare il tipo della class, e quindi usare solo static_cast <> o reinterpret_cast < >

    Per me la differenza è di circa 15 volte su MS VS 2005 C ++ SP1 non ottimizzato.

    Per un semplice controllo, RTTI può essere economico come un confronto tra puntatori. Per il controllo dell’ereditarietà, può essere costoso quanto un strcmp per ogni tipo in un albero di ereditarietà se si dynamic_cast -ing dall’alto verso il basso in un’implementazione là fuori.

    Puoi anche ridurre l’overhead non usando dynamic_cast e invece controllando esplicitamente il tipo tramite & typeid (…) == & typeid (type). Anche se questo non funziona necessariamente per .dlls o altro codice caricato dynamicmente, può essere abbastanza veloce per cose collegate staticamente.

    Anche se a quel punto è come usare un’istruzione switch, così ci sei.

    È sempre meglio misurare le cose. Nel seguente codice, sotto g ++, l’uso dell’identificazione del tipo codificato a mano sembra essere circa tre volte più veloce di RTTI. Sono sicuro che una implementtaion più realistica codificata a mano usando stringhe invece di caratteri sarebbe più lenta, portando i tempi vicini.

     #include  using namespace std; struct Base { virtual ~Base() {} virtual char Type() const = 0; }; struct A : public Base { char Type() const { return 'A'; } }; struct B : public Base {; char Type() const { return 'B'; } }; int main() { Base * bp = new A; int n = 0; for ( int i = 0; i < 10000000; i++ ) { #ifdef RTTI if ( A * a = dynamic_cast  ( bp ) ) { n++; } #else if ( bp->Type() == 'A' ) { A * a = static_cast (bp); n++; } #endif } cout << n << endl; } 

    Qualche tempo fa ho misurato i costi di tempo per RTTI nei casi specifici di MSVC e GCC per un PowerPC 3ghz. Nei test che ho eseguito (un’app per C ++ abbastanza grande con un albero di classi profonde), ogni dynamic_cast<> costa tra 0,8μs e 2μs, a seconda che sia stato colpito o perso.

    Quindi, quanto è costoso RTTI?

    Dipende interamente dal compilatore che stai usando. Comprendo che alcuni usano confronti tra stringhe e altri usano algoritmi reali.

    La tua unica speranza è scrivere un programma di esempio e vedere cosa fa il tuo compilatore (o almeno determinare quanto tempo ci vuole per eseguire un milione di dynamic_casts o un milione di typeid s).

    RTTI può essere economico e non necessita necessariamente di uno strcmp. Il compilatore limita il test per eseguire la gerarchia effettiva, in ordine inverso. Quindi se hai una class C che è figlia della class B che è figlia della class A, dynamic_cast da A * ptr a C * ptr implica solo un confronto tra puntatori e non due (BTW, solo il puntatore della tabella vptr è rispetto). Il test è come “if (vptr_of_obj == vptr_of_C) return (C *) obj”

    Un altro esempio, se proviamo a dynamic_cast da A * a B *. In tal caso, il compilatore controllerà entrambi i casi (obj è un C e obj è un B) a turno. Questo può anche essere semplificato in un singolo test (la maggior parte delle volte), poiché la tabella delle funzioni virtuali è fatta come aggregazione, quindi il test riprende da “if (offset_of (vptr_of_obj, B) == vptr_of_B)” con

    offset_of = return sizeof (vptr_table)> = sizeof (vptr_of_B)? vptr_of_new_methods_in_B: 0

    Il layout di memoria di

     vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ] 

    Come fa il compilatore a sapere di ottimizzarlo al momento della compilazione?

    Al momento della compilazione, il compilatore conosce la gerarchia attuale degli oggetti, quindi rifiuta di compilare una gerarchia di tipi diversi dynamic_casting. Quindi deve solo gestire la profondità della gerarchia e aggiungere la quantità invertita di test per abbinare tale profondità.

    Ad esempio, questo non viene compilato:

     void * something = [...]; // Compile time error: Can't convert from something to MyClass, no hierarchy relation MyClass * c = dynamic_cast(something); 

    RTTI può essere “costoso” perché hai aggiunto un’istruzione if ogni volta che esegui il confronto RTTI. In iterazioni profondamente annidate, questo può essere costoso. In qualcosa che non viene mai eseguito in un ciclo è essenzialmente gratuito.

    La scelta è quella di utilizzare un design polimorfico appropriato, eliminando l’if-statement. In loop profondamente annidati, questo è essenziale per le prestazioni. Altrimenti, non importa molto.

    Anche RTTI è costoso perché può oscurare la gerarchia delle sottoclassi (se ce n’è anche una). Può avere l’effetto collaterale di rimuovere “l’object orientato” da “programmazione orientata agli oggetti”.