Quel est le prix du RTTI?

152

Je comprends que l'utilisation de RTTI pose un problème de ressources, mais quelle est sa taille? Partout où j'ai regardé, il suffit de dire que "RTTI est cher", mais aucun d'entre eux ne donne réellement de référence ou de données quantitatives concernant la mémoire, le temps du processeur ou la vitesse.

Alors, quel est le prix du 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 , ce serait mieux si j'incluais ce que je fais réellement. J'utilise une classe pour transmettre des données de différentes longueurs et qui peuvent 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 traverser les différents niveaux tout en leur permettant d'agir complètement différemment.

D'après ce que j'ai compris, dynamic_castutilise RTTI, donc je me demandais dans quelle mesure il serait faisable de l'utiliser sur un système limité.

Cristián Romo
la source
1
Suite à votre montage - très souvent, lorsque je me retrouve à faire plusieurs cast dynamiques, je me rends compte que l'utilisation du modèle Visiteur redresse les choses. Cela pourrait-il fonctionner pour vous?
philsquared
4
Je vais le dire de cette façon - je viens de commencer à utiliser dynamic_casten C ++, et maintenant, 9 fois sur 10, lorsque je "casse" le programme avec le débogueur, il se brise dans la fonction interne de conversion dynamique. C'est sacrément lent.
user541686
3
RTTI = "informations de type d'exécution", d'ailleurs.
Noumenon

Réponses:

115

Quel que soit le compilateur, vous pouvez toujours économiser sur l'exécution si vous pouvez vous permettre de le faire

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.

Au-delà ... 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 selon lesquels l'auteur de la proposition devrait éviter le RTTI pour des raisons de conception. Cependant, il y a de bonnes raisons d'utiliser RTTI (principalement à cause de boost :: any). Cela à l'esprit, il est utile de connaître son utilisation réelle des ressources dans les implémentations courantes.

J'ai récemment fait un tas de recherches sur RTTI dans GCC.

tl; dr: RTTI dans GCC utilise un espace négligeable et typeid(a) == typeid(b)est très rapide, sur de nombreuses plates-formes (Linux, BSD et peut-être plates-formes embarquées, 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 fournisseur" [1], et utilise toujours cet ABI pour les cibles Linux et BSD [2]. Pour les plates-formes qui prennent en charge cette ABI et également une liaison faible, typeid()renvoie un objet cohérent et unique pour chaque type, même au-delà des limites de liaison dynamique. Vous pouvez tester &typeid(a) == &typeid(b), ou simplement vous fier au fait que le test portable typeid(a) == typeid(b)compare simplement un pointeur en interne.

Dans l'ABI préférée de GCC, une table vtable de classe contient toujours un pointeur vers une structure RTTI par type, bien qu'elle puisse ne pas être utilisée. Ainsi, un typeid()appel lui-même ne devrait coûter autant que toute autre recherche de vtable (le même que l'appel d'une fonction membre virtuelle), et le support RTTI ne devrait pas utiliser d'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, à 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 tous les cas, le changement de taille du binaire compilé doit refléter le changement d'utilisation de la mémoire d'exécution.

Une expérience rapide (en utilisant GCC 4.4.3 sur Ubuntu 10.04 64 bits) montre que cela augmente en-fno-rtti fait la taille binaire d'un programme de test simple de quelques centaines d'octets. Cela se produit de manière cohérente dans les combinaisons de et . Je ne sais pas pourquoi la taille augmenterait; une possibilité est que le code STL de GCC se comporte différemment sans RTTI (puisque les exceptions ne fonctionneront pas).-g-O3

[1] Connu sous le nom d'Itanium C ++ ABI, documenté à http://www.codesourcery.com/public/cxx-abi/abi.html . Les noms sont horriblement 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, y compris i686 / x86_64. Les commentaires dans la source interne de GCC et dans le code STL font référence à Itanium comme étant la "nouvelle" ABI contrairement à l '"ancienne" qu'ils utilisaient auparavant. Pire encore, le «nouveau» / Itanium ABI fait référence à toutes les versions disponibles via -fabi-version; «l'ancienne» ABI était antérieure à cette gestion des versions. GCC a adopté l'ABI Itanium / versioned / "new" dans la version 3.0; l '«ancien» ABI a été utilisé en 2.95 et avant, si je lis correctement leurs changelogs.

[2] Je n'ai trouvé aucune ressource répertoriant std::type_infola stabilité des objets par plateforme. Pour les compilateurs J'ai eu accès, je suit: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. Cette macro contrôle le comportement de operator==for std::type_infodans la STL de GCC, à partir de GCC 3.0. J'ai trouvé que mingw32-gcc obéit à l'ABI Windows C ++, où les std::type_infoobjets ne sont pas uniques pour un type parmi les DLL; typeid(a) == typeid(b)appels strcmpsous les couvertures. Je suppose que sur les cibles intégrées à programme unique comme AVR, où il n'y a pas de code à lier, les std::type_infoobjets sont toujours stables.

sbrudenell
la source
6
Les exceptions fonctionnent sans RTTI. (Vous êtes autorisé à lancer un intet il n'y a pas de vtable là-dedans :))
Billy ONeal
3
@Deduplicator: Et pourtant, quand je désactive RTTI dans mon compilateur, ils fonctionnent très bien. Désolé de te décevoir.
Billy ONeal
5
Le mécanisme de gestion des exceptions doit être capable de fonctionner avec n'importe quel type répondant à quelques exigences de base. Vous êtes libre de suggérer comment gérer le lancement et la capture d' exceptions de type arbitraire à travers les limites du module sans RTTI. Veuillez tenir compte du fait que la diffusion ascendante et descendante est requise.
Deduplicator
15
typeid (a) == typeid (b) n'est PAS la même chose que B * ba = dynamic_cast <B *> (& a). Essayez-le sur des objets avec héritage multiple en tant que niveau aléatoire sur l'arborescence de classes dérivée et vous trouverez que typeid () == typeid () ne donnera pas de résultat positif. dynamic_cast est le seul moyen de rechercher l'arbre d'héritage pour de vrai. Arrêtez de penser aux économies potentielles en désactivant RTTI et utilisez-le simplement. Si vous avez dépassé votre capacité, optimisez le gonflement de votre code. Essayez d'éviter d'utiliser dynamic_cast dans les boucles internes ou tout autre code critique pour les performances et tout ira bien.
mysticcoder
3
@mcoder C'est pourquoi l'article le déclare explicitement the latter necessarily involves traversing an inheritance tree plus comparisons. @CoryB Vous pouvez "vous permettre" de le faire lorsque vous n'avez pas besoin de prendre en charge la conversion à partir de l'ensemble de l'arborescence d'héritage. Par exemple, si vous voulez trouver tous les éléments de type X dans une collection, mais pas ceux qui dérivent de X, alors ce que vous devez utiliser est le premier. Si vous devez également rechercher toutes les instances dérivées, vous devrez utiliser cette dernière.
Aidiakapi
48

Peut-être que ces chiffres aideraient.

Je faisais un test rapide en utilisant ceci:

  • Horloge GCC () + Profiler de XCode.
  • 100 000 000 d'itérations de boucle.
  • 2 x 2,66 GHz Intel Xeon bicœur.
  • La classe en question est dérivée d'une seule classe de base.
  • 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 est juste mon code réel, car j'avais besoin de créer un objet de ce type avant de vérifier s'il est similaire à celui que j'ai déjà.

Sans optimisation

Pour lesquels les résultats étaient (j'ai fait la moyenne de 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.

La conclusion serait donc:

  • Pour les cas de cast simples sans optimisation, typeid()c'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.

La conclusion serait donc:

  • Pour les cas de cast simples avec optimisation, typeid()est presque x20 plus rapide que dyncamic_cast.

Graphique

entrez la description de l'image ici

Le code

Comme demandé dans les commentaires, le code est ci-dessous (un peu brouillon, mais fonctionne). «FastDelegate.h» est disponible à partir d' 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;
}
Izhaki
la source
1
Bien sûr, la distribution dynamique est plus générale - elle fonctionne si l'élément est plus dérivé. Par exemple, class a {}; class b : public a {}; class c : public b {};lorsque la cible est une instance de cfonctionnera correctement lors du test de la classe bavec dynamic_cast, mais pas avec la typeidsolution. Encore raisonnable cependant, +1
Billy ONeal
34
Ce benchmark est totalement faux avec les optimisations : la vérification de typeid est invariante de boucle et est déplacée hors de la boucle. Ce n'est pas du tout intéressant, c'est une analyse comparative de base non-non.
Réintégrer Monica le
3
@Kuba: Alors le benchmark est faux. Ce n'est pas une raison pour effectuer une analyse comparative avec les optimisations désactivées; c'est une raison pour écrire de meilleurs benchmarks.
Billy ONeal
3
encore une fois, c'est un échec. "Pour les cas de cast simples avec optimisation, typeid () est presque x20 plus rapide que dyncamic_cast." ils ne font pas la même chose. Il y a une raison pour laquelle dynamic_cast est plus lent.
mysticcoder
1
@KubaOber: total +1. c'est tellement classique. et il devrait être évident d'après le nombre de cycles que cela s'est produit.
v.oddou
38

Cela dépend de l'échelle 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, en haut de chaque objet qui a des fonctions virtuelles, il y a un pointeur vers une 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 utiliseraient ceci pour stocker un autre pointeur vers la structure type_info pour la classe.

Par exemple en 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 véritable argument contre RTTI est l'incapacité de devoir modifier le code partout à chaque fois que vous ajoutez une nouvelle classe dérivée. Au lieu d'instructions de commutation partout, intégrez-les dans des fonctions virtuelles. Cela déplace tout le code qui est différent entre les classes dans les classes elles-mêmes, de sorte qu'une nouvelle dérivation a juste besoin de remplacer toutes les fonctions virtuelles pour devenir une classe pleinement fonctionnelle. Si vous avez déjà eu à parcourir une grande base de code pour chaque fois que quelqu'un vérifie le type d'une classe et fait quelque chose de différent, vous apprendrez rapidement à rester à l'écart de ce style de programmation.

Si votre compilateur vous permet de désactiver totalement RTTI, les économies de taille de code qui en résultent peuvent être significatives, avec un si petit espace RAM. Le compilateur doit générer une structure type_info pour chaque classe avec une fonction virtuelle. Si vous désactivez RTTI, toutes ces structures n'ont pas besoin d'être incluses dans l'image exécutable.

Éclipse
la source
4
+1 pour expliquer pourquoi l'utilisation de RTTI est considérée comme une mauvaise décision de conception, ce n'était pas tout à fait clair pour moi auparavant.
aguazales
6
Cette réponse est une compréhension de bas niveau de la puissance du C ++. "En général" et "Dans la plupart des implémentations" utilisés libéralement signifie que vous ne réfléchissez pas à la façon d'utiliser correctement les fonctionnalités du langage. Les fonctions virtuelles et la réimplémentation du RTTI ne sont pas la solution. RTTI est la réponse. Parfois, vous voulez simplement savoir si un objet est d'un certain type. C'est pourquoi il est là! Vous perdez donc quelques Ko de RAM au profit de certaines structures type_info. Gee ...
mysticcoder
16

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 le simple fait d'utiliser un simple membre énuméré ferait l'affaire et éviterait le coût prétendument «élevé» du RTTI. J'étais sceptique si RTTI était en fait plus cher que la simple ifdéclaration qu'il introduit. Boy oh boy, c'est ça.

Il s'avère que RTTI est cher, beaucoup plus cher qu'une ifinstruction équivalente ou qu'une simple switchsur une variable primitive en C ++. La réponse de S.Lott n'est donc pas tout à fait correcte, il y a un coût supplémentaire pour RTTI, et ce n'est pas simplement dû à une ifdéclaration dans le mix. C'est parce que le RTTI est très cher.

Ce test a été effectué sur le compilateur Apple LLVM 5.0, avec les optimisations de stock activées (paramètres de mode de publication par défaut).

Donc, j'ai ci-dessous 2 fonctions, dont chacune détermine le type concret d'un objet via 1) RTTI ou 2) un simple commutateur. Il le fait 50 000 000 fois. Sans plus tarder, je vous présente les durées d'exécution relatives pour 50 000 000 d'exécutions.

entrez la description de l'image ici

C'est vrai, cela a dynamicCastspris 94% du temps d'exécution. Alors que le regularSwitchbloc n'a pris que 3,3% .

En bref: si vous pouvez vous permettre de vous connecter à un enumtype comme je l'ai fait ci-dessous, je le recommanderais probablement, si vous avez besoin de faire du RTTI et que les performances sont primordiales. Il ne faut définir le membre qu'une seule fois (assurez-vous de l'obtenir via tous les constructeurs ), et assurez-vous de ne jamais l'écrire par la suite.

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

La manière standard:

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

Le RTTI standard est coûteux car il repose sur une comparaison de chaînes sous-jacentes et donc la vitesse du RTTI peut varier en fonction de la longueur du nom de classe.

La raison pour laquelle les comparaisons de chaînes sont utilisées est de le faire fonctionner de manière cohérente à travers les limites de la bibliothèque / DLL. Si vous construisez votre application de manière statique 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 de fonctionner (ne donnera jamais de faux positif, mais peut donner de faux négatifs) mais peut être jusqu'à 15 fois plus rapide. Cela repose sur l'implémentation de typeid () pour fonctionner d'une certaine manière et tout ce que vous faites est de comparer un pointeur de caractère interne. C'est aussi parfois équivalent à:

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

Vous pouvez cependant utiliser un hybride en toute sécurité qui sera très rapide si les types correspondent, et ce sera le pire des cas pour les types inégalés:

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

Pour comprendre si vous avez besoin d'optimiser cela, vous devez voir combien de temps vous passez à obtenir un nouveau paquet, par rapport au temps nécessaire pour traiter le paquet. Dans la plupart des cas, une comparaison de chaînes ne sera probablement pas une surcharge. (en fonction de votre classe ou de votre 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) dans le cadre 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 d'environ 15 fois sur MS VS 2005 C ++ SP1 non optimisé.

Marius
la source
2
"Le RTTI standard est cher car il repose sur une comparaison de chaînes sous-jacente" - non, il n'y a rien de "Standard" à ce sujet; il est juste comment de votre mise en œuvre typeid::operatortravail . GCC sur une plate-forme prise en charge, par exemple, utilise déjà des comparaisons de char *s, sans forcer - gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/… . Bien sûr, votre façon de faire permet à MSVC de se comporter beaucoup mieux que celui par défaut sur votre plate-forme, alors bravo, et je ne sais pas quelles sont les «cibles» qui utilisent des pointeurs de manière native ... mais mon point est que le comportement de MSVC ne l'est en aucun cas "La norme".
underscore_d
7

Pour une simple vérification, RTTI peut être aussi bon marché qu'une comparaison avec un pointeur. Pour la vérification de l'héritage, cela peut être aussi coûteux que strcmppour chaque type dans une arborescence d'héritage si vous allez dynamic_castdu haut vers le bas dans une implémentation.

Vous pouvez également réduire la surcharge en n'utilisant pas dynamic_castet en vérifiant à la place 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, cela peut être assez rapide pour les éléments liés statiquement.

Bien qu'à ce stade, c'est comme utiliser une instruction switch, alors voilà.

MSN
la source
1
Avez-vous des références pour la version strcmp? Il semble extrêmement inefficace et imprécis d'utiliser strcmp pour une vérification de type.
JaredPar
Dans une mauvaise implémentation qui pourrait avoir plusieurs objets type_info par type, il pourrait implémenter bool type_info :: operator == (const type_info & x) const comme "! Strcmp (name (), x.name ())"
Greg Rogers
3
Entrez dans le désassemblage de dynamic_cast ou de typeid (). Operator == pour MSVC et vous frapperez un strcmp là-dedans. Je suppose que c'est là pour le cas horrible où vous comparez avec un type compilé dans un autre .dll. Et il utilise le nom mutilé, donc au moins c'est correct étant donné le même compilateur.
MSN
1
vous êtes censé faire "typeid (...) == typeid (type)" et ne pas comparer l'adresse
Johannes Schaub - litb
1
Mon point est que vous pouvez faire & typeid (...) == & typeid (bla) dès le début et que vous serez en sécurité. Cela peut ne rien faire d'utile puisque typeid (...) pourrait être généré sur la pile, mais si leurs adresses sont égales, alors leurs types sont égaux.
MSN
6

Il est toujours préférable de mesurer les choses. Dans le code suivant, sous g ++, l'utilisation de l'identification de type codée à la main 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, rapprochant 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;
}

la source
1
essayez de ne pas le faire avec dynamic_cast, mais avec typeid. cela pourrait accélérer les performances.
Johannes Schaub - litb
1
mais en utilisant dynamic_cast est plus réaliste, au moins regardant mon code
2
il fait autre chose: il vérifie aussi si bp pointe vers un type dérivé de A. votre == 'A' vérifie s'il pointe exactement vers un 'A'. Je pense aussi que le test est quelque peu injuste: le compilateur peut facilement voir que bp ne peut pas pointer vers quelque chose de différent de A. mais je pense qu'il n'est pas optimisé ici.
Johannes Schaub - litb le
de toute façon, j'ai testé votre code. et il me donne "0.016s" pour RTTI et "0.044s" pour les appels de fonction virtuelle. (en utilisant -O2)
Johannes Schaub - litb
bien que le changer pour utiliser typeid ne fait aucune différence ici (encore 0,016s)
Johannes Schaub - litb
4

Il y a quelque temps, j'ai mesuré les coûts de temps pour RTTI dans les cas spécifiques de MSVC et GCC pour un PowerPC 3 GHz. Dans les tests que j'ai exécutés (une application C ++ assez volumineuse avec un arbre de classes profond), chacun dynamic_cast<>coûte entre 0,8 μs et 2 μs, selon qu'il a réussi ou raté.

Crashworks
la source
2

Alors, quel est le prix du 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 au moins de déterminer combien de temps il faut pour exécuter un million dynamic_castsou un million de typeids).

Max Lybbert
la source
1

RTTI peut être bon marché et n'a pas nécessairement besoin d'un strcmp. Le compilateur limite le test pour exécuter 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 n'implique qu'une seule comparaison de pointeurs 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é en un seul test (la plupart du temps), car la table des fonctions virtuelles est une agrégation, donc le test reprend à "if (offset_of (vptr_of_obj, B) == vptr_of_B)" avec

offset_of = return 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 pour optimiser cela au moment de la compilation?

Au moment de la compilation, le compilateur connaît la hiérarchie actuelle des objets, il refuse donc de compiler une hiérarchie de types différente dynamic_casting. Ensuite, il lui suffit de gérer la profondeur de la hiérarchie et d'ajouter le nombre de tests inversé pour correspondre à 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);  
X-Ryl669
la source
-5

RTTI peut être "coûteux" car vous avez ajouté une instruction if à chaque fois que vous effectuez la comparaison RTTI. Dans les itérations profondément imbriquées, cela peut être coûteux. Dans quelque chose qui n'est jamais exécuté en boucle, c'est essentiellement gratuit.

Le choix est d'utiliser une conception polymorphe appropriée, en éliminant l'instruction if. Dans les boucles profondément imbriquées, c'est essentiel pour les performances. Sinon, cela n'a pas beaucoup d'importance.

Le RTTI est également coûteux car il peut masquer la hiérarchie des sous-classes (s'il y en a une). Cela peut avoir pour effet secondaire de supprimer "orienté objet" de la "programmation orientée objet".

S.Lott
la source
2
Pas nécessairement - j'allais l'utiliser indirectement via dynamic_cast, et garder la hiérarchie en place, car je dois abaisser parce que chaque sous-type doit avoir des données différentes (de taille variable) qui doivent être appliquées différemment, d'où dynamic_cast.
Cristián Romo
1
@ Cristián Romo: Veuillez mettre à jour votre question avec ces nouveaux faits. dynamic_cast est un mal (parfois) nécessaire en C ++. Poser des questions sur les performances RTTI lorsque vous êtes obligé de le faire n'a pas beaucoup de sens.
S.Lott
@ S.Lott: mis à jour. Désolé pour la confusion.
Cristián Romo
1
J'ai fait une expérience à ce sujet tout à l'heure - il s'avère que RTTI est beaucoup plus cher que l' ifinstruction que vous introduisez lorsque vous vérifiez les informations de type d'exécution de cette façon.
bobobobo