Wie teuer ist RTTI?

152

Ich verstehe, dass die Verwendung von RTTI einen Ressourcenhit darstellt, aber wie groß ist dieser? Überall, wo ich gesucht habe, heißt es nur, dass "RTTI teuer ist", aber keiner von ihnen gibt tatsächlich Benchmarks oder quantitative Daten an, die Speicher, Prozessorzeit oder Geschwindigkeit regeln.

Wie teuer ist RTTI? Ich könnte es auf einem eingebetteten System verwenden, auf dem ich nur 4 MB RAM habe, also zählt jedes Bit.

Bearbeiten: Gemäß der Antwort von S. Lott wäre es besser, wenn ich einbeziehe, was ich tatsächlich tue. Ich verwende eine Klasse, um Daten unterschiedlicher Länge zu übergeben, die unterschiedliche Aktionen ausführen können. Daher wäre es schwierig, dies nur mit virtuellen Funktionen zu tun. Es scheint, dass die Verwendung einiger dynamic_casts dieses Problem beheben könnte, indem die verschiedenen abgeleiteten Klassen durch die verschiedenen Ebenen geleitet werden und dennoch völlig anders agieren können.

dynamic_castNach meinem Verständnis wird RTTI verwendet, daher habe ich mich gefragt, wie machbar es wäre, es auf einem begrenzten System zu verwenden.

Cristián Romo
quelle
1
Nach Ihrer Bearbeitung - sehr oft, wenn ich mehrere dynamische Casts mache, stelle ich fest, dass die Verwendung des Besuchermusters die Dinge wieder gerade macht. Könnte das für Sie funktionieren?
Philsquared
4
Ich werde es so ausdrücken - ich habe gerade angefangen, es dynamic_castin C ++ zu verwenden, und jetzt, 9 von 10 Mal, wenn ich das Programm mit dem Debugger "kaputt mache", bricht es innerhalb der internen Dynamic-Cast-Funktion. Es ist verdammt langsam.
user541686
3
RTTI = "Laufzeitinformationen" übrigens.
Noumenon

Antworten:

115

Unabhängig vom Compiler können Sie jederzeit Laufzeit sparen, wenn Sie es sich leisten können

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

anstatt

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

Ersteres beinhaltet nur einen Vergleich von std::type_info; Letzteres beinhaltet notwendigerweise das Durchlaufen eines Vererbungsbaums plus Vergleiche.

Darüber hinaus ... wie jeder sagt, ist die Ressourcennutzung implementierungsspezifisch.

Ich stimme den Kommentaren aller anderen zu, dass der Einreicher RTTI aus Designgründen vermeiden sollte. Es gibt jedoch gute Gründe, RTTI zu verwenden (hauptsächlich wegen boost :: any). Vor diesem Hintergrund ist es hilfreich, die tatsächliche Ressourcennutzung in gängigen Implementierungen zu kennen.

Ich habe kürzlich eine Reihe von Untersuchungen zu RTTI in GCC durchgeführt.

tl; dr: RTTI in GCC verwendet vernachlässigbaren Speicherplatz und typeid(a) == typeid(b)ist auf vielen Plattformen (Linux, BSD und möglicherweise eingebettete Plattformen, aber nicht mingw32) sehr schnell. Wenn Sie wissen, dass Sie immer auf einer gesegneten Plattform sind, ist RTTI nahezu kostenlos.

Grobe Details:

GCC bevorzugt die Verwendung eines bestimmten "herstellerneutralen" C ++ - ABI [1] und verwendet diesen ABI immer für Linux- und BSD-Ziele [2]. Gibt für Plattformen, die diesen ABI und auch eine schwache Verknüpfung unterstützen, typeid()ein konsistentes und eindeutiges Objekt für jeden Typ zurück, auch über dynamische Verknüpfungsgrenzen hinweg. Sie können testen &typeid(a) == &typeid(b)oder sich einfach darauf verlassen, dass der tragbare Test typeid(a) == typeid(b)tatsächlich nur einen Zeiger intern vergleicht.

In GCCs bevorzugter ABI enthält eine Klasse vtable immer einen Zeiger auf eine RTTI-Struktur pro Typ, obwohl sie möglicherweise nicht verwendet wird. Ein typeid()Aufruf selbst sollte also nur so viel kosten wie jede andere vtable-Suche (genau wie das Aufrufen einer virtuellen Member-Funktion), und die RTTI-Unterstützung sollte keinen zusätzlichen Speicherplatz für jedes Objekt verwenden.

Soweit ich das beurteilen kann, enthalten die von GCC verwendeten RTTI-Strukturen (dies sind alle Unterklassen von std::type_info) neben dem Namen nur wenige Bytes für jeden Typ. Mir ist nicht klar, ob die Namen im Ausgabecode auch mit vorhanden sind -fno-rtti. In beiden Fällen sollte die Änderung der Größe der kompilierten Binärdatei die Änderung der Laufzeitspeichernutzung widerspiegeln.

Ein kurzes Experiment (unter Verwendung von GCC 4.4.3 unter Ubuntu 10.04 64-Bit) zeigt, dass die Binärgröße eines einfachen Testprogramms -fno-rttitatsächlich um einige hundert Bytes erhöht wird . Dies geschieht konsistent über Kombinationen von -gund -O3. Ich bin mir nicht sicher, warum die Größe zunehmen würde; Eine Möglichkeit besteht darin, dass sich der STL-Code von GCC ohne RTTI anders verhält (da Ausnahmen nicht funktionieren).

[1] Bekannt als Itanium C ++ ABI, dokumentiert unter http://www.codesourcery.com/public/cxx-abi/abi.html . Die Namen sind schrecklich verwirrend: Der Name bezieht sich auf die ursprüngliche Entwicklungsarchitektur, obwohl die ABI-Spezifikation auf vielen Architekturen einschließlich i686 / x86_64 funktioniert. Kommentare in der internen Quelle und im STL-Code von GCC beziehen sich auf Itanium als "neuen" ABI im Gegensatz zu dem "alten", den sie zuvor verwendet haben. Schlimmer noch, der "neue" / Itanium ABI bezieht sich auf alle Versionen, die über erhältlich sind -fabi-version. Das "alte" ABI war älter als diese Versionierung. GCC hat das Itanium / versioned / "new" ABI in Version 3.0 übernommen. Das "alte" ABI wurde in Version 2.95 und früher verwendet, wenn ich ihre Änderungsprotokolle richtig lese.

[2] Ich konnte keine Ressourcen finden, die die std::type_infoObjektstabilität nach Plattform auflisten . Für Compiler, auf die ich Zugriff hatte, habe ich Folgendes verwendet : echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. Dieses Makro steuert das Verhalten von operator==for std::type_infoin der STL von GCC ab GCC 3.0. Ich habe festgestellt, dass mingw32-gcc dem Windows C ++ - ABI folgt, bei dem std::type_infoObjekte für einen Typ in DLLs nicht eindeutig sind. typeid(a) == typeid(b)Anrufe strcmpunter der Decke. Ich spekuliere, dass auf eingebetteten Einzelprogrammzielen wie AVR, bei denen es keinen Code zum Verknüpfen gibt, std::type_infoObjekte immer stabil sind.

sbrudenell
quelle
6
Ausnahmen funktionieren ohne RTTI. (Du intdarfst eine werfen und es gibt keine vtable darin :))
Billy ONeal
3
@Deduplicator: Und doch, wenn ich RTTI in meinem Compiler ausschalte, funktionieren sie einwandfrei. Es tut mir leid dich zu enttäuschen.
Billy ONeal
5
Der Mechanismus zur Behandlung von Ausnahmen muss mit jedem Typ arbeiten können, der einige grundlegende Anforderungen erfüllt. Es steht Ihnen frei, Vorschläge zum Auslösen und Abfangen von Ausnahmen beliebigen Typs über Modulgrenzen hinweg ohne RTTI zu machen. Bitte beachten Sie, dass Up- und Downcasting erforderlich ist.
Deduplikator
15
typeid (a) == typeid (b) ist NICHT dasselbe wie B * ba = dynamic_cast <B *> (& a). Versuchen Sie es mit Objekten mit Mehrfachvererbung als zufällige Ebene im abgeleiteten Klassenbaum, und Sie werden feststellen, dass typeid () == typeid () kein positives Ergebnis liefert. dynamic_cast ist die einzige Möglichkeit, den Vererbungsbaum nach real zu durchsuchen. Denken Sie nicht mehr über mögliche Einsparungen nach, indem Sie RTTI deaktivieren, und verwenden Sie es einfach. Wenn Sie überlastet sind, optimieren Sie Ihren Code Bloat. Versuchen Sie zu vermeiden, dynamic_cast in inneren Schleifen oder anderen leistungskritischen Codes zu verwenden, und es wird Ihnen gut gehen.
Mysticcoder
3
@mcoder Deshalb heißt es in dem Artikel ausdrücklich the latter necessarily involves traversing an inheritance tree plus comparisons. @CoryB Sie können es sich "leisten", wenn Sie das Casting aus dem gesamten Vererbungsbaum nicht unterstützen müssen. Wenn Sie beispielsweise alle Elemente vom Typ X in einer Sammlung suchen möchten, jedoch nicht diejenigen, die von X abgeleitet sind, sollten Sie das erstere verwenden. Wenn Sie auch alle abgeleiteten Instanzen finden müssen, müssen Sie letztere verwenden.
Aidiakapi
48

Vielleicht würden diese Zahlen helfen.

Ich habe einen kurzen Test damit gemacht:

  • GCC Clock () + XCode Profiler.
  • 100.000.000 Schleifeniterationen.
  • 2 x 2,66 GHz Dual-Core Intel Xeon.
  • Die betreffende Klasse wird von einer einzelnen Basisklasse abgeleitet.
  • typeid (). name () gibt "N12fastdelegate13FastDelegate1IivEE" zurück.

5 Fälle wurden getestet:

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 ist nur mein eigentlicher Code, da ich ein Objekt dieses Typs erstellen musste, bevor ich prüfen konnte, ob es einem bereits vorhandenen ähnlich ist.

Ohne Optimierung

Für die die Ergebnisse waren (ich habe ein paar Läufe gemittelt):

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.

Die Schlussfolgerung wäre also:

  • Für einfache Cast-Fälle ohne Optimierung typeid()ist mehr als doppelt so schnell wie dyncamic_cast.
  • Auf einer modernen Maschine beträgt der Unterschied zwischen beiden etwa 1 Nanosekunde (eine Millionstel Millisekunde).

Mit Optimierung (-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.

Die Schlussfolgerung wäre also:

  • Für einfache Cast-Fälle mit Optimierung typeid()ist fast x20 schneller als dyncamic_cast.

Diagramm

Geben Sie hier die Bildbeschreibung ein

Der Code

Wie in den Kommentaren angefordert, ist der Code unten (etwas chaotisch, funktioniert aber). ‚FastDelegate.h‘ ist erhältlich hier .

#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;
}
Izhaki
quelle
1
Natürlich ist die dynamische Besetzung allgemeiner - sie funktioniert, wenn der Gegenstand mehr abgeleitet ist. ZB class a {}; class b : public a {}; class c : public b {};wenn das Ziel eine Instanz von cist, funktioniert es gut, wenn Sie auf Klasse bmit testen dynamic_cast, aber nicht mit der typeidLösung. Immer noch vernünftig, +1
Billy ONeal
34
Dieser Benchmark ist völlig falsch mit Optimierungen : Die Typprüfung ist schleifeninvariant und wird aus der Schleife verschoben. Es ist überhaupt nicht interessant, es ist ein grundlegendes Benchmarking-Nein-Nein.
Stellen Sie Monica
3
@ Kuba: Dann ist der Benchmark falsch. Dies ist kein Grund für ein Benchmarking mit deaktivierten Optimierungen. Das ist ein Grund, bessere Benchmarks zu schreiben.
Billy ONeal
3
Dies ist wiederum ein Fehler. "Für einfache Cast-Fälle mit Optimierung ist typeid () fast x20 schneller als dyncamic_cast." Sie tun nicht das Gleiche. Es gibt einen Grund, warum dynamic_cast langsamer ist.
Mysticcoder
1
@ KubaOber: insgesamt +1. das ist so klassisch. und es sollte aus dem Aussehen der Zyklusnummer ersichtlich sein, dass dies geschah.
v.oddou
38

Es kommt auf das Ausmaß der Dinge an. Zum größten Teil sind es nur ein paar Überprüfungen und ein paar Zeiger-Dereferenzen. In den meisten Implementierungen befindet sich oben auf jedem Objekt mit virtuellen Funktionen ein Zeiger auf eine vtable, die eine Liste von Zeigern auf alle Implementierungen der virtuellen Funktion in dieser Klasse enthält. Ich würde vermuten, dass die meisten Implementierungen dies verwenden würden, um entweder einen anderen Zeiger auf die type_info-Struktur für die Klasse zu speichern.

Zum Beispiel 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);       
}

Im Allgemeinen ist das eigentliche Argument gegen RTTI die Unwartbarkeit, dass Code jedes Mal geändert werden muss, wenn Sie eine neue abgeleitete Klasse hinzufügen. Berücksichtigen Sie diese Anweisungen nicht überall, sondern in virtuelle Funktionen. Dadurch wird der gesamte Code, der zwischen den Klassen unterschiedlich ist, in die Klassen selbst verschoben, sodass bei einer neuen Ableitung nur alle virtuellen Funktionen überschrieben werden müssen, um eine voll funktionsfähige Klasse zu werden. Wenn Sie jemals jedes Mal, wenn jemand den Typ einer Klasse überprüft und etwas anderes tut, eine große Codebasis durchsuchen mussten, werden Sie schnell lernen, sich von diesem Programmierstil fernzuhalten.

Wenn Sie mit Ihrem Compiler RTTI vollständig deaktivieren können, kann die endgültige Einsparung an Codegröße bei einem so kleinen RAM-Speicher erheblich sein. Der Compiler muss für jede einzelne Klasse mit einer virtuellen Funktion eine type_info-Struktur generieren. Wenn Sie RTTI deaktivieren, müssen alle diese Strukturen nicht im ausführbaren Image enthalten sein.

Finsternis
quelle
4
+1 für die tatsächliche Erklärung, warum die Verwendung von RTTI als schlechte Designentscheidung angesehen wird, war mir vorher nicht ganz klar.
Aguazales
6
Diese Antwort ist ein einfaches Verständnis der Leistungsfähigkeit von C ++. "Im Allgemeinen" und "In den meisten Implementierungen", die großzügig verwendet werden, bedeuten, dass Sie nicht darüber nachdenken, wie Sie die Sprachfunktionen gut nutzen können. Virtuelle Funktionen und die Neuimplementierung von RTTI sind nicht die Antwort. RTTI ist die Antwort. Manchmal möchten Sie nur wissen, ob ein Objekt ein bestimmter Typ ist. Deshalb ist es da! Sie verlieren also ein paar KB RAM an einige type_info-Strukturen. Gee ...
Mysticcoder
16

Nun, der Profiler lügt nie.

Da ich eine ziemlich stabile Hierarchie von 18 bis 20 Typen habe, die sich nicht sehr ändert, habe ich mich gefragt, ob die Verwendung eines einfachen enum'd-Mitglieds den Trick machen und die angeblich "hohen" Kosten von RTTI vermeiden würde. Ich war skeptisch, ob RTTI tatsächlich teurer war als nur die ifdarin eingeführte Aussage. Junge, Junge, ist es das?

Es stellt sich heraus , dass RTTI ist teuer, viel teurer als eine gleichwertige ifAnweisung oder einem einfachen switchauf eine primitive Variable in C ++. Die Antwort von S.Lott ist also nicht ganz richtig, es gibt zusätzliche Kosten für RTTI und es liegt nicht nur daran eine ifAussage in der Mischung enthalten ist. Das liegt daran, dass RTTI sehr teuer ist.

Dieser Test wurde auf dem Apple LLVM 5.0-Compiler mit aktivierten Bestandsoptimierungen durchgeführt (Standardeinstellungen für den Release-Modus).

Ich habe also unten 2 Funktionen, von denen jede den konkreten Typ eines Objekts entweder über 1) RTTI oder 2) einen einfachen Schalter ermittelt. Dies geschieht 50.000.000 Mal. Ohne weiteres präsentiere ich Ihnen die relativen Laufzeiten für 50.000.000 Läufe.

Geben Sie hier die Bildbeschreibung ein

Das ist richtig, das dynamicCastshat 94% der Laufzeit gekostet. Während der regularSwitchBlock nur 3,3% nahm .

Lange Rede, kurzer Sinn: Wenn Sie sich die Energie leisten können, einen enumTyp wie unten einzuhängen, würde ich ihn wahrscheinlich empfehlen, wenn Sie RTTI durchführen müssen und die Leistung von größter Bedeutung ist. Das Mitglied muss nur einmal festgelegt werden (stellen Sie sicher, dass Sie es über alle Konstruktoren erhalten ), und schreiben Sie es anschließend nie mehr.

Das heißt, dies sollte Ihre OOP-Praktiken nicht durcheinander bringen. Es soll nur verwendet werden, wenn Typinformationen einfach nicht verfügbar sind und Sie sich in der Verwendung von RTTI befinden.

#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 ) ;
}
Bobobobo
quelle
13

Der Standardweg:

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

Standard-RTTI ist teuer, da ein zugrunde liegender Zeichenfolgenvergleich durchgeführt werden muss und daher die Geschwindigkeit von RTTI abhängig von der Länge des Klassennamens variieren kann.

Der Grund, warum Zeichenfolgenvergleiche verwendet werden, besteht darin, dass sie über Bibliotheks- / DLL-Grenzen hinweg konsistent funktionieren. Wenn Sie Ihre Anwendung statisch erstellen und / oder bestimmte Compiler verwenden, können Sie wahrscheinlich Folgendes verwenden:

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

Was garantiert nicht funktioniert (niemals ein falsches Positiv ergibt, aber möglicherweise ein falsches Negativ ergibt), aber bis zu 15-mal schneller sein kann. Dies hängt von der Implementierung von typeid () ab, um auf eine bestimmte Weise zu funktionieren, und Sie vergleichen lediglich einen internen Zeichenzeiger. Dies ist manchmal auch gleichbedeutend mit:

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

Sie können jedoch einen Hybrid sicher verwenden, der sehr schnell ist, wenn die Typen übereinstimmen, und der schlechteste Fall für nicht übereinstimmende Typen ist:

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

Um zu verstehen, ob Sie dies optimieren müssen, müssen Sie sehen, wie viel Zeit Sie damit verbringen, ein neues Paket zu erhalten, verglichen mit der Zeit, die für die Verarbeitung des Pakets benötigt wird. In den meisten Fällen ist ein Zeichenfolgenvergleich wahrscheinlich kein großer Aufwand. (abhängig von Ihrer Klasse oder Ihrem Namespace :: Klassennamenlänge)

Der sicherste Weg, dies zu optimieren, besteht darin, Ihre eigene Typ-ID als int (oder als Aufzählungstyp: int) als Teil Ihrer Basisklasse zu implementieren und damit den Typ der Klasse zu bestimmen. Verwenden Sie dann einfach static_cast <> oder reinterpret_cast < >

Für mich beträgt der Unterschied bei nicht optimiertem MS VS 2005 C ++ SP1 ungefähr das 15-fache.

Marius
quelle
2
"Standard RTTI ist teuer, weil es auf einem zugrunde liegenden String-Vergleich beruht" - nein, daran ist nichts "Standard"; es ist nur , wie Sie Ihre Implementierung der typeid::operatorArbeit . GCC auf einer unterstützten Plattform verwendet beispielsweise bereits Vergleiche von char *s, ohne dass wir dies erzwingen - gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/… . Sicher, Ihr Weg bewirkt, dass sich MSVC auf Ihrer Plattform viel besser verhält als der Standard, also ein großes Lob, und ich weiß nicht, was die "einige Ziele" sind, die Zeiger nativ verwenden ... aber mein Punkt ist, dass das Verhalten von MSVC in keiner Weise ist "Standard".
underscore_d
7

Für eine einfache Überprüfung kann RTTI so billig sein wie ein Zeigervergleich. Für die Vererbungsprüfung kann es so teuer sein wie strcmpfür jeden Typ in einem Vererbungsbaum, wenn Sie es sinddynamic_cast in einer Implementierung von oben nach unten arbeiten.

Sie können den Overhead auch reduzieren, indem Sie ihn nicht verwenden dynamic_cast indem Sie den Typ nicht explizit über & typeid (...) == & typeid (type) und stattdessen explizit überprüfen. Während dies nicht unbedingt für DLLs oder anderen dynamisch geladenen Code funktioniert, kann es für Dinge, die statisch verknüpft sind, recht schnell sein.

Obwohl es zu diesem Zeitpunkt wie die Verwendung einer switch-Anweisung ist, können Sie loslegen.

MSN
quelle
1
Haben Sie Referenzen für die strcmp-Version? Es scheint äußerst ineffizient und ungenau, strcmp für eine Typprüfung zu verwenden.
JaredPar
In einer schlechten Implementierung, die mehrere type_info-Objekte pro Typ haben könnte, könnte bool type_info :: operator == (const type_info & x) const als "! Strcmp (name (), x.name ())" implementiert werden
Greg Rogers
3
Betreten Sie die Disassemblierung von dynamic_cast oder typeid (). Operator == für MSVC und Sie werden dort einen strcmp treffen. Ich gehe davon aus, dass es für den schrecklichen Fall da ist, in dem Sie mit einem Typ vergleichen, der in einer anderen DLL kompiliert wurde. Und es wird der verstümmelte Name verwendet, so dass es zumindest bei demselben Compiler korrekt ist.
MSN
1
Sie sollten "typeid (...) == typeid (type)" tun und nicht die Adresse vergleichen
Johannes Schaub - litb
1
Mein Punkt ist, dass Sie & typeid (...) == & typeid (blah) frühzeitig ausführen können und sicher sind. Es kann sein, dass es nichts Nützliches tut, da typeid (...) auf dem Stapel generiert werden könnte, aber wenn ihre Adressen gleich sind, sind ihre Typen gleich.
MSN
6

Es ist immer am besten, Dinge zu messen. Im folgenden Code scheint unter g ++ die Verwendung der handcodierten Typidentifikation etwa dreimal schneller zu sein als bei RTTI. Ich bin sicher, dass eine realistischere handcodierte Implementierung mit Strings anstelle von Zeichen langsamer wäre und die Timings nahe beieinander liegen würde.

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

quelle
1
Versuchen Sie es nicht mit dynamic_cast, sondern mit typeid. es könnte die Leistung beschleunigen.
Johannes Schaub - Litb
1
aber unter Verwendung von dynamic_cast ist realistischer, zumindest in meinem Code suchen
2
es macht eine andere Sache: Es prüft auch, ob bp auf einen von A abgeleiteten Typ zeigt. Ihr == 'A' prüft, ob es genau auf ein 'A' zeigt. Ich denke auch, dass der Test etwas unfair ist: Der Compiler kann leicht erkennen, dass bp nicht auf etwas anderes als A verweisen kann, aber ich denke, dass er hier nicht optimiert wird.
Johannes Schaub - litb
Trotzdem habe ich deinen Code getestet. und es gibt mir "0.016s" für RTTI und "0.044s" für die virtuellen Funktionsaufrufe. (mit -O2)
Johannes Schaub - litb
Obwohl die Änderung auf typeid hier keinen Unterschied macht (immer noch 0,016 s)
Johannes Schaub - litb
4

Vor einiger Zeit habe ich die Zeitkosten für RTTI in den speziellen Fällen von MSVC und GCC für einen 3-GHz-PowerPC gemessen. In den Tests, die ich ausgeführt habe (eine ziemlich große C ++ - App mit einem tiefen Klassenbaum), dynamic_cast<>kostete jeder zwischen 0,8 μs und 2 μs, je nachdem, ob er traf oder verfehlte.

Crashworks
quelle
2

Wie teuer ist RTTI?

Das hängt ganz vom verwendeten Compiler ab. Ich verstehe, dass einige Zeichenfolgenvergleiche verwenden, andere echte Algorithmen.

Ihre einzige Hoffnung besteht darin, ein Beispielprogramm zu schreiben und zu sehen, was Ihr Compiler tut (oder zumindest zu bestimmen, wie viel Zeit es dauert, eine Million dynamic_castsoder eine Million auszuführentypeid s ).

Max Lybbert
quelle
1

RTTI kann billig sein und benötigt nicht unbedingt einen strcmp. Der Compiler begrenzt den Test, um die tatsächliche Hierarchie in umgekehrter Reihenfolge auszuführen. Wenn Sie also eine Klasse C haben, die ein Kind der Klasse B ist, das ein Kind der Klasse A ist, impliziert dynamic_cast von einem A * ptr zu einem C * ptr nur einen Zeigervergleich und nicht zwei (Übrigens ist nur der vptr-Tabellenzeiger verglichen). Der Test lautet wie folgt: "if (vptr_of_obj == vptr_of_C) return (C *) obj"

Ein weiteres Beispiel, wenn wir versuchen, dynamic_cast von A * nach B * zu übertragen. In diesem Fall überprüft der Compiler nacheinander beide Fälle (obj ist ein C und obj ist ein B). Dies kann auch auf einen einzelnen Test vereinfacht werden (meistens), da die virtuelle Funktionstabelle als Aggregation erstellt wird, sodass der Test mit "if (offset_of (vptr_of_obj, B) == vptr_of_B)" mit fortgesetzt wird

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

Das Speicherlayout von

vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ]

Woher weiß der Compiler, wie er dies zur Kompilierungszeit optimieren kann?

Zur Kompilierungszeit kennt der Compiler die aktuelle Hierarchie von Objekten, sodass er es ablehnt, eine andere Typhierarchie für dynamic_casting zu kompilieren. Dann muss es nur noch die Hierarchietiefe verarbeiten und die invertierte Anzahl von Tests hinzufügen, um dieser Tiefe zu entsprechen.

Dies wird beispielsweise nicht kompiliert:

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

RTTI kann "teuer" sein, da Sie jedes Mal, wenn Sie den RTTI-Vergleich durchführen, eine if-Anweisung hinzugefügt haben. In tief verschachtelten Iterationen kann dies teuer sein. In etwas, das niemals in einer Schleife ausgeführt wird, ist es im Wesentlichen kostenlos.

Sie können ein geeignetes polymorphes Design verwenden, um die if-Anweisung zu eliminieren. In tief verschachtelten Schleifen ist dies für die Leistung wesentlich. Ansonsten spielt es keine große Rolle.

RTTI ist auch teuer, weil es die Unterklassenhierarchie verdecken kann (falls es überhaupt eine gibt). Es kann den Nebeneffekt haben, das "objektorientierte" aus der "objektorientierten Programmierung" zu entfernen.

S.Lott
quelle
2
Nicht unbedingt - ich wollte es indirekt über dynamic_cast verwenden und die Hierarchie beibehalten, da ich einen Downcast durchführen muss, da jeder Subtyp unterschiedliche (unterschiedlich große) Daten haben muss, die unterschiedlich angewendet werden müssen, daher dynamic_cast.
Cristián Romo
1
@ Cristián Romo: Bitte aktualisieren Sie Ihre Frage mit diesen neuen Fakten. dynamic_cast ist ein (manchmal) notwendiges Übel in C ++. Es macht wenig Sinn, nach der RTTI-Leistung zu fragen, wenn Sie dazu gezwungen sind.
S.Lott
@ S.Lott: Aktualisiert. Entschuldigung für die Verwirrung.
Cristián Romo
1
Ich habe gerade ein Experiment dazu durchgeführt - es stellt sich heraus, dass RTTI erheblich teurer ist als die ifAnweisung, die Sie einführen, wenn Sie die Informationen zum Laufzeittyp auf diese Weise überprüfen.
Bobobobo