Quanto costa RTTI?


152

Capisco che c'è un impatto sulle risorse dall'uso di RTTI, ma quanto è grande? Ovunque abbia guardato dice semplicemente che "RTTI è costoso", ma nessuno di loro in realtà fornisce parametri di riferimento o dati quantitativi per la memoria, il tempo del processore o la velocità.

Quindi, quanto costa RTTI? Potrei usarlo su un sistema incorporato in cui ho solo 4 MB di RAM, quindi ogni bit conta.

Modifica: secondo la risposta di S. Lott , sarebbe meglio se includessi quello che sto effettivamente facendo. Sto usando una classe per trasmettere dati di diverse lunghezze e che possono eseguire azioni diverse , quindi sarebbe difficile farlo usando solo funzioni virtuali. Sembra che l'uso di alcuni dynamic_casts possa risolvere questo problema consentendo alle diverse classi derivate di passare attraverso i diversi livelli pur consentendo loro di agire in modo completamente diverso.

Dalla mia comprensione, dynamic_castutilizza RTTI, quindi mi chiedevo quanto sarebbe fattibile utilizzare su un sistema limitato.


1
A seguito della tua modifica, molto spesso quando mi ritrovo a fare diversi cast dinamici, mi rendo conto che l'uso del modello Visitor raddrizza di nuovo le cose. Potrebbe funzionare per te?
philsquared il

4
Lo dirò in questo modo: ho appena iniziato a utilizzare dynamic_castin C ++ e ora, 9 volte su 10 quando "rompo" il programma con il debugger, si rompe all'interno della funzione di cast dinamico interno. È dannatamente lento.
user541686,

3
RTTI = "informazioni sul tipo di runtime", comunque.
Noumenon,

Risposte:


115

Indipendentemente dal compilatore, puoi sempre risparmiare in fase di esecuzione se puoi permetterti di farlo

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

invece di

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

Il primo prevede solo un confronto di std::type_info; il secondo implica necessariamente l'attraversamento di un albero ereditario e confronti.

Passato che ... come dicono tutti, l'utilizzo delle risorse è specifico dell'implementazione.

Concordo con i commenti di tutti gli altri sul fatto che il mittente dovrebbe evitare RTTI per motivi di progettazione. Tuttavia, ci sono buoni motivi per usare RTTI (principalmente a causa di boost :: any). Ciò in mente, è utile conoscere il suo effettivo utilizzo delle risorse nelle implementazioni comuni.

Di recente ho fatto molte ricerche su RTTI in GCC.

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

Dettagli grintosi:

GCC preferisce utilizzare un particolare ABI C ++ "vendor-neutral" [1] e utilizza sempre questo ABI per target Linux e BSD [2]. Per piattaforme che supportano questa ABI e anche un collegamento debole, typeid()restituisce un oggetto coerente e unico per ogni tipo, anche attraverso i limiti di collegamento dinamico. Puoi testare &typeid(a) == &typeid(b)o semplicemente fare affidamento sul fatto che il test portatile typeid(a) == typeid(b)confronta effettivamente un puntatore internamente.

Nell'ABI preferito di GCC, una vtable di classe contiene sempre un puntatore a una struttura RTTI per tipo, sebbene non possa essere utilizzata. Quindi una typeid()chiamata stessa dovrebbe costare solo quanto qualsiasi altra ricerca vtable (lo stesso che chiamare una funzione membro virtuale) e il supporto RTTI non dovrebbe usare spazio extra per ogni oggetto.

Da quello che posso capire, le strutture RTTI utilizzate 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 output anche con -fno-rtti. In entrambi i casi, la modifica delle dimensioni 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 a 64 bit) mostra che in -fno-rttirealtà aumenta la dimensione binaria di un semplice programma di test di alcune centinaia di byte. Ciò accade in modo coerente tra le combinazioni di -ge -O3. Non sono sicuro del motivo per cui la dimensione aumenterebbe; una possibilità è che il codice STL di GCC si comporti diversamente senza RTTI (poiché le eccezioni non funzioneranno).

[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 la specifica ABI funzioni su molte architetture tra cui i686 / x86_64. I commenti nella fonte interna di GCC e nel codice STL si riferiscono a Itanium come alla "nuova" ABI in contrasto con la "vecchia" che avevano usato prima. Peggio ancora, il "nuovo" / Itanium ABI si riferisce a tutte le versioni disponibili attraverso -fabi-version; la "vecchia" ABI ha preceduto questo controllo delle versioni. GCC ha adottato l'ABI Itanium / versioned / "new" nella versione 3.0; la "vecchia" ABI è stata utilizzata nella versione 2.95 e precedenti, se sto leggendo correttamente i loro log delle modifiche.

[2] Non sono riuscito a trovare alcuna std::type_infostabilità dell'oggetto che elenca le risorse per piattaforma. Per compilatori Ho avuto accesso a, ho usato il seguente: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. Questa macro controlla il comportamento di operator==for std::type_infoin STL di GCC, a partire da GCC 3.0. Ho scoperto che mingw32-gcc obbedisce all'ABI di Windows C ++, in cui gli std::type_infooggetti non sono univoci per un tipo tra le DLL; typeid(a) == typeid(b)chiama strcmpsotto le coperte. Ho ipotizzato che su target embedded a programma singolo come AVR, dove non esiste un codice a cui collegarsi, gli std::type_infooggetti sono sempre stabili.


6
Le eccezioni funzionano senza RTTI. (Ti è permesso lanciare un inte non c'è alcuna tabella di ciò :))
Billy ONeal

3
@Deduplicator: Eppure, quando spengo RTTI nel mio compilatore, funzionano bene. Dispiace deludervi.
Billy ONeal,

5
Il meccanismo di gestione delle eccezioni deve essere in grado di funzionare con qualsiasi tipo soddisfacendo alcuni requisiti di base. Sei libero di suggerire come gestire il lancio e la cattura di eccezioni di tipo arbitrario oltre i confini del modulo senza RTTI. Si prega di considerare che è richiesto il casting up e down.
Deduplicatore,

15
typeid (a) == typeid (b) NON è uguale a B * ba = dynamic_cast <B *> (& a). Provalo su oggetti con ereditarietà multipla come livello casuale nell'albero delle classi derivato e troverai typeid () == typeid () non produrrà un positivo. dynamic_cast è l'unico modo per cercare l'albero delle eredità per reale. Smetti di pensare a potenziali risparmi disabilitando RTTI e semplicemente utilizzandolo. Se hai una capacità eccessiva, ottimizza il tuo codice in eccesso. Cerca di evitare l'uso di dynamic_cast all'interno di circuiti interni o di qualsiasi altro codice critico per le prestazioni e andrà tutto bene.
mysticcoder il

3
@mcoder Ecco perché l'articolo lo afferma esplicitamente the latter necessarily involves traversing an inheritance tree plus comparisons. @CoryB Puoi "permetterti" di farlo quando non hai bisogno di supportare il casting dell'intero albero ereditario. Ad esempio, se vuoi trovare tutti gli elementi di tipo X in una raccolta, ma non quelli che derivano da X, allora quello che dovresti usare è il primo. Se è necessario trovare anche tutte le istanze derivate, è necessario utilizzare quest'ultima.
Aidiakapi,

48

Forse queste cifre potrebbero aiutare.

Stavo facendo un rapido test usando questo:

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

Sono stati testati 5 casi:

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, poiché avevo bisogno di creare un oggetto di quel tipo prima di verificare se è simile a quello che ho già.

Senza ottimizzazione

Per cui i risultati sono stati (ho fatto una media di alcune prove):

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 casi di cast semplici senza ottimizzazione typeid()è 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 le semplici custodie con ottimizzazione, typeid()è quasi 20 volte più veloce di dyncamic_cast.

Grafico

inserisci qui la descrizione dell'immagine

Il codice

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

#include <iostream>
#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;
}

1
Naturalmente, il cast dinamico è più generale: funziona se l'elemento è più derivato. Ad esempio, class a {}; class b : public a {}; class c : public b {};quando il target è un'istanza di cfunzionerà bene durante i test per la classe bcon dynamic_cast, ma non con la typeidsoluzione. Comunque ragionevole, +1
Billy ONeal

34
Questo benchmark è completamente fasullo con ottimizzazioni : il controllo di tipo è invariante e viene spostato fuori dal ciclo. Non è affatto interessante, è un benchmark di base no-no.
Ripristina Monica il

3
@Kuba: Quindi il benchmark è falso. Questo non è un motivo per fare il benchmark con le ottimizzazioni disattivate; questo è un motivo per scrivere benchmark migliori.
Billy ONeal,

3
ancora una volta, questo è un fallimento. "Per casi di cast semplici con ottimizzazione, typeid () è quasi 20 volte più veloce di dyncamic_cast." NON fanno la stessa cosa. C'è un motivo per cui dynamic_cast è più lento.
mysticcoder,

1
@KubaOber: +1 totale. questo è così classico. e dovrebbe essere ovvio dall'aspetto del numero di cicli che ciò è accaduto.
v.oddou,

38

Dipende dalla scala delle cose. Per la maggior parte sono solo un paio di controlli e alcune dereferenze del puntatore. Nella maggior parte delle implementazioni, nella parte superiore di ogni oggetto che ha funzioni virtuali, c'è un puntatore a una vtable che contiene un elenco di puntatori a tutte le implementazioni della funzione virtuale su quella classe. Immagino che la maggior parte delle implementazioni userebbe questo per memorizzare un altro puntatore alla struttura type_info per la classe.

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<vtable *>(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 classe derivata. Invece di scambiare le istruzioni ovunque, trasformale in funzioni virtuali. Questo sposta tutto il codice che è diverso tra le classi nelle classi stesse, quindi una nuova derivazione deve solo sovrascrivere tutte le funzioni virtuali per diventare una classe pienamente funzionante. Se hai mai dovuto cercare una grande base di codice ogni volta che qualcuno controlla il tipo di una classe e fa qualcosa di diverso, imparerai rapidamente a stare lontano da quello stile di programmazione.

Se il tuo compilatore ti consente di disattivare completamente RTTI, il risparmio finale sulla dimensione del codice risultante può essere significativo, con uno spazio RAM così ridotto. Il compilatore deve generare una struttura type_info per ogni singola classe con una funzione virtuale. Se si disattiva RTTI, non è necessario includere tutte queste strutture nell'immagine eseguibile.


4
+1 per aver effettivamente spiegato perché l'utilizzo di RTTI è considerato una cattiva decisione di progettazione, che non mi era abbastanza chiaro prima.
aguazales,

6
Questa risposta è una comprensione di basso livello della potenza del C ++. "In generale" e "Nella maggior parte delle implementazioni" usati liberamente indicano che non stai pensando a come usare bene le funzionalità delle lingue. Le funzioni virtuali e la reimplementazione di RTTI non sono la risposta. RTTI è la risposta. A volte vuoi solo sapere se un oggetto è un certo tipo. Ecco perché è lì! Quindi perdi qualche KB di RAM per alcune strutture type_info. Accidenti ...
mysticcoder il

16

Bene, il profiler non mente mai.

Dal momento che ho una gerarchia piuttosto stabile di 18-20 tipi che non sta cambiando molto, mi chiedevo se il solo uso di un semplice membro enum avrebbe fatto il trucco ed evitasse il presunto "alto" costo di RTTI. Ero scettico se RTTI fosse effettivamente più costoso della semplice ifdichiarazione che introduce. Ragazzo oh ragazzo, vero?

Si scopre che RTTI è costoso, molto più costoso di un'istruzione equivalente ifo di una semplice switchsu una variabile primitiva in C ++. Quindi la risposta di S.Lott non è del tutto corretta, ci sono costi extra per RTTI e non è dovuto al fatto che ifhai solo una dichiarazione nel mix. È dovuto al fatto che RTTI è molto costoso.

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

Quindi, ho sotto 2 funzioni, ognuna delle quali determina il tipo concreto di un oggetto tramite 1) RTTI o 2) un semplice interruttore. Lo fa 50.000.000 di volte. Senza ulteriori indugi, vi presento i relativi tempi di funzionamento per 50.000.000 di corse.

inserisci qui la descrizione dell'immagine

Proprio così, l' dynamicCastsha preso il 94% del tempo di esecuzione. Mentre il regularSwitchblocco ha preso solo il 3,3% .

Per farla breve: se puoi permetterti di agganciare un enumtipo di tipo come ho fatto sotto, probabilmente lo consiglierei, se hai bisogno di fare RTTI e le prestazioni sono di primaria importanza. Ci vuole solo una volta l'impostazione del membro (assicurarsi di ottenerlo tramite tutti i costruttori ) e assicurarsi di non scriverlo mai dopo.

Detto questo, fare ciò non dovrebbe incasinare le tue pratiche OOP .. è pensato solo per essere usato quando le informazioni sul tipo semplicemente non sono disponibili e ti trovi ad affrontare l'utilizzazione di RTTI.

#include <stdio.h>
#include <vector>
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<Animal*> &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<Dog*>( an ) )
        dogs++;
      else if( dynamic_cast<BigCat*>( an ) )
        bigcats++;
      else if( dynamic_cast<Cat*>( an ) )
        cats++;
      else //if( dynamic_cast<Animal*>( 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<Animal*> &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<Animal*> 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 ) ;
}

13

Il modo standard:

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

Lo standard RTTI è costoso perché si basa sul confronto di stringhe sottostanti e quindi la velocità di RTTI può variare a seconda della lunghezza del nome della classe.

Il motivo per cui vengono utilizzati i confronti di stringhe è farlo funzionare in modo coerente attraverso i limiti della libreria / DLL. Se si crea l'applicazione in modo statico e / o si utilizzano determinati compilatori, è possibile utilizzare probabilmente:

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

Che 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 si basa sull'implementazione di typeid () per funzionare in un certo modo e tutto ciò che stai facendo è confrontare un puntatore a caratteri interno. Questo a volte equivale anche a:

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

È possibile comunque utilizzare un ibrido in modo sicuro che sarà molto veloce se i tipi di corrispondenza, e sarà in caso peggiore per i tipi senza eguali:

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

Per capire se è necessario ottimizzarlo, è necessario vedere quanto tempo si impiega per ottenere un nuovo pacchetto, rispetto al tempo necessario per elaborare il pacchetto. Nella maggior parte dei casi, un confronto tra stringhe probabilmente non sarà un grande sovraccarico. (a seconda della classe o dello spazio dei nomi :: lunghezza del nome della classe)

Il modo più sicuro per ottimizzarlo è implementare il proprio typeid come int (o enum Type: int) come parte della classe di base e utilizzarlo per determinare il tipo di classe, quindi utilizzare static_cast <> o reinterpret_cast < >

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


2
"Lo standard RTTI è costoso perché si basa sul confronto di stringhe sottostanti" - no, non c'è nulla di "standard" al riguardo; è proprio come l'implementazione del typeid::operatorlavoro di s . GCC su una piattaforma supportata, ad esempio, utilizza già confronti di char *s, senza che noi lo forziamo - gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/… . Certo, la tua strada fa in modo che MSVC si comporti molto meglio del default sulla tua piattaforma, quindi complimenti, e non so quali siano gli "alcuni obiettivi" che usano i puntatori in modo nativo ... ma il mio punto è che il comportamento di MSVC non è in alcun modo "Standard".
underscore_d

7

Per un semplice controllo, RTTI può essere economico come un confronto puntatore. Per il controllo dell'ereditarietà, può essere costoso come strcmpper ogni tipo in un albero ereditario se si sta dynamic_casteseguendo dall'alto verso il basso in un'implementazione là fuori.

Puoi anche ridurre il sovraccarico non usando dynamic_caste invece controllando esplicitamente il tipo tramite & typeid (...) == & typeid (tipo). Sebbene ciò non funzioni necessariamente per .dlls o altro codice caricato dinamicamente, può essere abbastanza veloce per cose che sono staticamente collegate.

Anche se a quel punto è come usare un'istruzione switch, quindi eccoti.


1
Hai dei riferimenti per la versione strcmp? Sembra estremamente inefficiente e impreciso utilizzare strcmp per un controllo del tipo.
JaredPar,

In una cattiva implementazione che potrebbe avere più oggetti type_info per tipo, potrebbe implementare bool type_info :: operator == (const type_info & x) const come "! Strcmp (name (), x.name ())"
Greg Rogers,

3
Passa allo smontaggio di dynamic_cast o typeid (). Operator == per MSVC e ti colpirai uno strcmp. Presumo che sia lì per il caso orribile in cui si sta confrontando un tipo compilato in un altro .dll. E usa il nome alterato, quindi almeno è corretto dato lo stesso compilatore.
MSN

1
dovresti fare "typeid (...) == typeid (type)" e non confrontare l'indirizzo
Johannes Schaub - litb

1
Il mio punto è che puoi fare & typeid (...) == & typeid (blah) fin da subito e sarai al sicuro. In realtà potrebbe non fare nulla di utile poiché typeid (...) potrebbe essere generato nello stack, ma se i loro indirizzi sono uguali, allora i loro tipi sono uguali.
MSN

6

È sempre meglio misurare le cose. Nel codice seguente, sotto g ++, l'uso dell'identificazione del tipo con codifica manuale sembra essere circa tre volte più veloce di RTTI. Sono sicuro che un'implementazione codificata a mano più realistica usando stringhe anziché caratteri sarebbe più lenta, avvicinando i tempi.

#include <iostream>
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 <A*> ( bp ) ) {
            n++;
        }
#else
        if ( bp->Type() == 'A' ) {
            A * a = static_cast <A*>(bp);
            n++;
        }
#endif
    }
    cout << n << endl;
}

1
prova a non farlo con dynamic_cast, ma con typeid. potrebbe accelerare le prestazioni.
Johannes Schaub - litb

1
ma utilizzando dynamic_cast è più realistico, almeno guardando il mio codice

2
fa una cosa diversa: controlla anche se bp punta a un tipo derivato da A. your == 'A' controlla se punta esattamente ad una 'A'. penso anche che il test sia in qualche modo ingiusto: il compilatore può facilmente vedere bp non può indicare qualcosa di diverso da A. ma penso che non ottimizzi qui.
Johannes Schaub - litb

comunque, ho testato il tuo codice. e mi dà "0,016" per RTTI e "0,044" per le chiamate di funzione virtuale. (usando -O2)
Johannes Schaub - litb

anche se cambiarlo per usare typeid non fa alcuna differenza qui (ancora 0,016s)
Johannes Schaub - litb

4

Qualche tempo fa ho misurato i costi del tempo per RTTI nei casi specifici di MSVC e GCC per un PowerPC da 3ghz. Nei test ho eseguito (un'app C ++ abbastanza grande con un albero di classe profonda), ognuno dynamic_cast<>costava tra 0,8 μs e 2μs, a seconda che colpisse o meno.


2

Quindi, quanto costa RTTI?

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

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


1

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 classe C che è una figlia della classe B che è una figlia della classe A, dynamic_cast da un A * ptr a un C * ptr implica solo un confronto puntatore 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 essendo una C e obj essendo una B) a turno. Questo può anche essere semplificato per un singolo test (la maggior parte delle volte), poiché la tabella delle funzioni virtuali viene creata come aggregazione, quindi il test riprende a "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 in fase di compilazione?

Al momento della compilazione, il compilatore conosce l'attuale gerarchia di oggetti, quindi rifiuta di compilare gerarchie di tipo diverso dynamic_casting. Quindi deve solo gestire la profondità della gerarchia e aggiungere la quantità inversa di test per corrispondere a 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<MyClass*>(something);  

-5

RTTI può essere "costoso" perché hai aggiunto un'istruzione if ogni volta che esegui il confronto RTTI. Nelle 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 corretto design polimorfico, eliminando l'istruzione if. Nei loop profondamente annidati, questo è essenziale per le prestazioni. Altrimenti, non importa molto.

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


2
Non necessariamente: lo avrei usato indirettamente tramite dynamic_cast e avrei mantenuto la gerarchia in atto, perché ho bisogno di effettuare il downcast perché ogni sottotipo deve avere dati diversi (dimensioni variabili) che devono essere applicati in modo diverso, quindi dynamic_cast.
Cristián Romo,

1
@ Cristián Romo: aggiorna la tua domanda con questi nuovi fatti. dynamic_cast è un male (a volte) necessario in C ++. Chiedere prestazioni RTTI quando si è costretti a farlo non ha molto senso.
S. Lott,

@ S.Lott: aggiornato. Mi dispiace per la confusione.
Cristián Romo,

1
Ho fatto un esperimento su questo proprio ora: risulta che RTTI è significativamente più costoso ifdell'affermazione che introduci quando controlli le informazioni sul tipo di runtime in questo modo.
Bobobobo,
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.