web-dev-qa-db-fra.com

Combien coûte RTTI?

Je comprends que l’utilisation de RTTI constitue une ressource importante, mais quelle est sa taille? Partout où j'ai regardé, je me suis contenté de dire que "le RTTI coûte cher", mais aucun d'entre eux ne donne de référence ni de données quantitatives sur la mémoire, le temps processeur ou la vitesse.

Alors, combien coûte RTTI? Je pourrais l'utiliser sur un système embarqué où je n'ai que 4 Mo de RAM, donc chaque bit compte.

Edit: Selon la réponse de S. Lott , il serait préférable d’inclure ce que je suis en train de faire. J'utilise une classe pour transmettre des données de différentes longueurs et pouvant effectuer différentes actions , il serait donc difficile de le faire en utilisant uniquement des fonctions virtuelles. Il semble que l’utilisation de quelques dynamic_casts pourrait remédier à ce problème en permettant aux différentes classes dérivées de passer à travers les différents niveaux tout en leur permettant néanmoins d'agir complètement différemment.

De ma compréhension, dynamic_cast utilise RTTI, donc je me demandais s’il serait faisable de l’utiliser sur un système limité.

144
Cristián Romo

Quel que soit le compilateur, vous pouvez toujours économiser sur le temps d’exécution si vous pouvez vous le permettre.

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

au lieu de

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

Le premier implique une seule comparaison de std::type_info; ce dernier implique nécessairement de parcourir un arbre d'héritage plus des comparaisons.

Passé cela ... comme tout le monde le dit, l'utilisation des ressources est spécifique à l'implémentation.

Je suis d'accord avec les commentaires de tous les autres que l'expéditeur devrait éviter RTTI pour des raisons de conception. Cependant, il existe de bonnes raisons d'utiliser RTTI (principalement à cause de boost :: any). Dans cet esprit, il est utile de connaître son utilisation réelle des ressources dans les implémentations courantes.

J'ai récemment effectué plusieurs recherches sur le RTTI à GCC.

tl; dr: RTTI dans GCC utilise un espace négligeable et typeid(a) == typeid(b) est très rapide, sur de nombreuses plateformes (Linux, BSD et peut-être même, mais pas mingw32). Si vous savez que vous serez toujours sur une plate-forme bénie, RTTI est très proche de la gratuité.

Détails granuleux:

GCC préfère utiliser un ABI C++ "indépendant du vendeur" particulier, et utilise toujours cet ABI pour les cibles Linux et BSD [2]. Pour les plates-formes prenant en charge cette ABI et une liaison faible, typeid() renvoie un objet cohérent et unique pour chaque type, même au-delà des limites de liaison dynamiques. Vous pouvez tester &typeid(a) == &typeid(b), ou simplement vous appuyer sur le fait que le test portable typeid(a) == typeid(b) ne fait que comparer un pointeur en interne.

Dans l'ABI préférée de GCC, une classe vtable contient toujours un pointeur sur une structure RTTI par type, bien qu'elle ne soit peut-être pas utilisée. Ainsi, un typeid() appelant lui-même ne devrait que coûter autant que toute autre recherche vtable (identique à l'appel d'une fonction membre virtuelle), et le support RTTI ne devrait pas utiliser aucun espace supplémentaire pour chaque objet.

D'après ce que je peux comprendre, les structures RTTI utilisées par GCC (ce sont toutes les sous-classes de std::type_info) Ne contiennent que quelques octets pour chaque type, mis à part le nom. Il n'est pas clair pour moi si les noms sont présents dans le code de sortie même avec -fno-rtti. Dans les deux cas, la modification de la taille du fichier binaire compilé doit refléter la modification de l'utilisation de la mémoire d'exécution.

Une expérimentation rapide (utilisant GCC 4.4.3 sur Ubuntu 10.04 64 bits) montre que -fno-rtti Augmente réellement la taille binaire d’un simple programme de test de quelques centaines d'octets. Cela se produit de manière uniforme dans les combinaisons de -g Et de -O3. Je ne sais pas pourquoi la taille augmenterait; Une possibilité est que le code STL de GCC se comporte différemment sans RTTI (car les exceptions ne fonctionneront pas).

[1] Connu sous le nom d'IBI Itanium C++, documenté à l'adresse http://www.codesourcery.com/public/cxx-abi/abi.html . Les noms sont terriblement déroutants: le nom fait référence à l'architecture de développement d'origine, bien que la spécification ABI fonctionne sur de nombreuses architectures, notamment i686/x86_64. Les commentaires dans la source interne et le code STL de GCC désignent Itanium comme le "nouveau" ABI, contrairement à "l'ancien" qu'ils utilisaient auparavant. Pire encore, le "nouveau"/Itanium ABI fait référence à toutes les versions disponibles via -fabi-version; le "vieux" ABI était antérieur à ce versionnage. GCC a adopté l’ABI Itanium/versioned/"new" dans la version 3.0; le "vieux" ABI a été utilisé dans les versions 2.95 et antérieures, si je lis correctement leur journal des modifications.

[2] Je n'ai trouvé aucune ressource répertoriant la stabilité des objets std::type_info Par plate-forme. Pour les compilateurs auxquels j'avais accès, j'ai utilisé ce qui suit: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. Cette macro contrôle le comportement de operator== Pour std::type_info Dans la STL de GCC, à partir de GCC 3.0. J'ai constaté que mingw32-gcc obéissait à l'ABI Windows C++, où les objets std::type_info N'étaient pas uniques pour un type parmi plusieurs DLL; typeid(a) == typeid(b) appelle strcmp sous les couvertures. Je spécule que sur les cibles intégrées à programme unique telles qu'AVR, où il n'y a pas de code à associer, les objets std::type_info Sont toujours stables.

113
sbrudenell

Peut-être que ces chiffres pourraient aider.

Je faisais un test rapide en utilisant ceci:

  • GCC Clock () + Profiler de XCode.
  • 100 000 000 itérations de boucle.
  • 2 processeurs Intel Xeon double cœur 2,66 GHz.
  • La classe en question est dérivée d'une classe de base unique.
  • typeid (). name () renvoie "N12fastdelegate13FastDelegate1IivEE"

5 cas ont été testés:

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 n'est que mon code, car je devais créer un objet de ce type avant de vérifier s'il était similaire à celui que j'ai déjà.

Sans optimisation

Pour lesquels les résultats ont été (j'ai moyenné quelques essais):

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.

Donc, la conclusion serait:

  • Pour les transtypages simples sans optimisation, typeid() est plus de deux fois plus rapide que dyncamic_cast.
  • Sur une machine moderne, la différence entre les deux est d'environ 1 nanoseconde (un millionième de milliseconde).

Avec optimisation (-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.

Donc, la conclusion serait:

  • Pour les cas simples avec optimisation, typeid() est presque 20 fois plus rapide que dyncamic_cast.

Graphique

enter image description here

Le code

Comme demandé dans les commentaires, le code est ci-dessous (un peu en désordre, mais fonctionne). 'FastDelegate.h' est disponible à partir de ici .

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

Cela dépend de l'ampleur des choses. Pour la plupart, il ne s'agit que de quelques vérifications et de quelques déréférences de pointeur. Dans la plupart des implémentations, au sommet de chaque objet ayant des fonctions virtuelles, il y a un pointeur sur une table vtable qui contient une liste de pointeurs vers toutes les implémentations de la fonction virtuelle sur cette classe. Je suppose que la plupart des implémentations l'utilisent pour stocker un autre pointeur sur la structure type_info de la classe.

Par exemple dans 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);       
}

En général, le vrai argument contre RTTI est l'inconvénient de devoir modifier le code partout à chaque fois que vous ajoutez une nouvelle classe dérivée. Au lieu de passer des instructions partout, intégrez-les dans des fonctions virtuelles. Cela déplace tout le code différent entre les classes dans les classes elles-mêmes, de sorte qu'une nouvelle dérivation doit simplement remplacer toutes les fonctions virtuelles pour devenir une classe pleinement opérationnelle. Si vous avez déjà eu à parcourir une base de code volumineuse à chaque fois que quelqu'un vérifie le type d'une classe et fait quelque chose de différent, vous apprendrez rapidement à rester en dehors de ce style de programmation.

Si votre compilateur vous permet de désactiver totalement RTTI, les économies finales en termes de taille de code peuvent être importantes, avec un espace aussi petit RAM. Le compilateur doit générer une structure type_info pour chaque classe avec une fonction virtuelle: si vous désactivez RTTI, il n'est pas nécessaire d'inclure toutes ces structures dans l'image exécutable.

37
Eclipse

Eh bien, le profileur ne ment jamais.

Comme j'ai une hiérarchie assez stable de 18 à 20 types qui ne change pas beaucoup, je me suis demandé si un simple membre enum'd ferait le truc et éviter le coût prétendument "élevé" de RTTI. J'étais sceptique si RTTI était en fait plus cher que la déclaration if qu'il introduit. Oh mon Dieu, c'est ça?.

Il se trouve que RTTI is cher, beaucoup plus cher qu'une déclaration équivalente if ou un simple switch sur une variable primitive en C++ . Donc la réponse de S.Lott n’est pas tout à fait correcte, il y a un coût supplémentaire pour RTTI, et c’est pas en raison du fait que il ne reste qu'une déclaration if dans le mixage. C'est parce que RTTI est très cher.

Ce test a été effectué sur le Apple LLVM 5.0, avec l'optimisation des stocks activée (paramètres de mode de publication par défaut)).

Donc, j'ai ci-dessous 2 fonctions, chacune desquelles figure le type concret d'un objet soit via 1) RTTI ou 2) un simple commutateur. Il le fait 50 000 000 fois. Sans plus tarder, je vous présente les temps d'exécution relatifs pour 50 000 000 de courses.

enter image description here

C'est vrai, le dynamicCasts a pris 94% d'exécution. Alors que le bloc regularSwitch ne prenait que .3%.

Bref récit: si vous avez les moyens d’acheter un type enum comme je l’ai fait ci-dessous, je le recommanderais probablement si vous devez utiliser RTTI et la performance est primordiale. Il suffit de définir le membre une fois (assurez-vous de l'obtenir via tous les constructeurs ), et assurez-vous de ne jamais l'écrire après.

Ceci dit, cela ne devrait pas gâcher vos pratiques OOP pratiques .. il est uniquement destiné à être utilisé lorsque les informations de type ne sont tout simplement pas disponibles et que vous vous retrouvez coincé dans en utilisant 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 ) ;
}
16
bobobobo

La manière standard:

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

Le format RTTI standard est coûteux car il repose sur une comparaison de chaîne sous-jacente. Par conséquent, sa vitesse peut varier en fonction de la longueur du nom de classe.

La comparaison de chaînes est utilisée pour le rendre cohérent, quelle que soit la limite de la bibliothèque/DLL. Si vous construisez votre application statiquement et/ou que vous utilisez certains compilateurs, vous pouvez probablement utiliser:

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

Ce qui n'est pas garanti pour fonctionner (ne donnera jamais de faux positif, mais peut donner de faux négatifs) mais peut être jusqu'à 15 fois plus rapide. Cela dépend de l'implémentation de typeid () pour fonctionner d'une certaine manière et vous ne faites que comparer un pointeur de caractère interne. C'est aussi parfois équivalent à:

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

Vous pouvez utilisez cependant un hybride en toute sécurité qui sera très rapide si les types correspondent, et sera le pire des cas pour les types non appariés:

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

Pour comprendre si vous devez optimiser cela, vous devez voir combien de temps vous passez à obtenir un nouveau paquet, par rapport au temps nécessaire pour le traiter. Dans la plupart des cas, une comparaison de chaînes ne constituera probablement pas une surcharge importante. (en fonction de votre classe ou de l'espace de noms :: longueur du nom de la classe)

Le moyen le plus sûr d’optimiser cela est d’implémenter votre propre typeid en tant qu’int (ou enum Type: int) en tant que partie de votre classe de base et de l’utiliser pour déterminer le type de la classe, puis d’utiliser simplement static_cast <> ou reinterpret_cast < >

Pour moi, la différence est environ 15 fois supérieure à celle de MS VS 2005 C++ SP1 non optimisé.

13
Marius

Pour un contrôle simple, RTTI peut être aussi bon marché qu'une comparaison de pointeur. Pour la vérification d'héritage, cela peut coûter aussi cher qu'un strcmp pour chaque type dans un arbre d'héritage si vous êtes dynamic_cast- de haut en bas en une seule mise en œuvre.

Vous pouvez également réduire les frais généraux en n'utilisant pas dynamic_cast et en vérifiant le type explicitement via & typeid (...) == & typeid (type). Bien que cela ne fonctionne pas nécessairement pour les fichiers .dll ou tout autre code chargé dynamiquement, il peut être assez rapide pour les éléments liés statiquement.

Bien que, à ce stade, cela ressemble à utiliser une instruction switch, alors voilà.

6
MSN

Il est toujours préférable de mesurer les choses. Dans le code suivant, sous g ++, l’utilisation de l’identification de type avec codage manuel semble être environ trois fois plus rapide que RTTI. Je suis sûr qu’une implémentation codée à la main plus réaliste utilisant des chaînes au lieu de caractères serait plus lente, ce qui rapprocherait les timings.

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

Il y a quelque temps, j’ai mesuré les coûts de temps pour RTTI dans les cas spécifiques de MSVC et de GCC pour un PowerPC de 3 GHz. Dans les tests que j’ai exécutés (une application C++ assez volumineuse avec un arbre de classes profond), chaque dynamic_cast<> coûtent entre 0.8μs et 2μs, qu’il soit touché ou non.

4
Crashworks

Alors, combien coûte RTTI?

Cela dépend entièrement du compilateur que vous utilisez. Je comprends que certains utilisent des comparaisons de chaînes et d’autres utilisent de vrais algorithmes.

Votre seul espoir est d'écrire un exemple de programme et de voir ce que fait votre compilateur (ou du moins de déterminer le temps nécessaire pour exécuter un million dynamic_casts ou un million typeids).

2
Max Lybbert

RTTI peut être bon marché et ne nécessite pas forcément de strcmp. Le compilateur limite le test pour effectuer la hiérarchie réelle, dans l'ordre inverse. Donc, si vous avez une classe C qui est un enfant de la classe B qui est un enfant de la classe A, dynamic_cast d’un A * ptr à un C * ptr implique seulement une comparaison de pointeur et non deux (BTW, seul le pointeur de table vptr est par rapport). Le test est comme "if (vptr_of_obj == vptr_of_C) return (C *) obj"

Un autre exemple, si nous essayons de dynamic_cast de A * à B *. Dans ce cas, le compilateur vérifiera les deux cas (obj étant un C et obj étant un B) à tour de rôle. Cela peut également être simplifié à un seul test (la plupart du temps), car la table de fonction virtuelle est une agrégation, de sorte que le test reprend à "if (offset_of (vptr_of_obj, B) == vptr_of_B)"

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

La disposition de la mémoire de

vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ]

Comment le compilateur sait-il l'optimiser au moment de la compilation?

Lors de la compilation, le compilateur connaît la hiérarchie actuelle des objets et refuse de compiler une hiérarchie de types différente. Dynamic_casting. Ensuite, il suffit de gérer la profondeur de la hiérarchie et d’ajouter le nombre de tests inverses correspondant à cette profondeur.

Par exemple, cela ne compile pas:

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