¿Qué tan caro es RTTI?

152

Entiendo que hay un impacto en los recursos por usar RTTI, pero ¿qué tan grande es? En todas partes que he visto, solo dice que "RTTI es costoso", pero ninguno de ellos ofrece puntos de referencia o datos cuantitativos sobre memoria, tiempo de procesador o velocidad.

Entonces, ¿qué tan caro es RTTI? Podría usarlo en un sistema integrado donde solo tengo 4 MB de RAM, por lo que cada bit cuenta.

Editar: Según la respuesta de S. Lott , sería mejor si incluyo lo que realmente estoy haciendo. Estoy usando una clase para pasar datos de diferentes longitudes y que puede realizar diferentes acciones , por lo que sería difícil hacerlo usando solo funciones virtuales. Parece que usar unos pocos dynamic_casts podría remediar este problema al permitir que las diferentes clases derivadas pasen a través de los diferentes niveles y aún así permitirles actuar de manera completamente diferente.

Según tengo entendido, dynamic_castusa RTTI, así que me preguntaba qué tan factible sería usarlo en un sistema limitado.

Cristián Romo
fuente
1
Después de su edición, muy a menudo cuando me encuentro haciendo varios lanzamientos dinámicos, me doy cuenta de que usar el patrón Visitante endereza las cosas nuevamente. ¿Eso podría funcionar para ti?
philsquared
44
Lo pondré de esta manera: recién comencé a usar dynamic_casten C ++, y ahora, 9 de cada 10 veces cuando "rompo" el programa con el depurador, se rompe dentro de la función interna de conversión dinámica. Es muy lento.
user541686
3
RTTI = "información de tipo de tiempo de ejecución", por cierto.
Noumenon

Respuestas:

115

Independientemente del compilador, siempre puede ahorrar en tiempo de ejecución si puede permitirse hacer

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

en vez de

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

El primero implica solo una comparación de std::type_info; lo último implica necesariamente atravesar un árbol de herencia más comparaciones.

Más allá de eso ... como todos dicen, el uso de los recursos es específico de la implementación.

Estoy de acuerdo con los comentarios de todos los demás de que el remitente debe evitar RTTI por razones de diseño. Sin embargo, no son buenas razones para utilizar RTTI (principalmente a causa de impulso :: hubiera). En mente, es útil saber su uso real de recursos en implementaciones comunes.

Recientemente hice un montón de investigación sobre RTTI en GCC.

tl; dr: RTTI en GCC utiliza un espacio insignificante y typeid(a) == typeid(b)es muy rápido, en muchas plataformas (Linux, BSD y quizás plataformas integradas, pero no mingw32). Si sabe que siempre estará en una plataforma bendecida, RTTI está muy cerca de ser gratuito.

Detalles arenosos:

GCC prefiere usar un ABI C ++ "vendedor neutral" particular [1], y siempre usa este ABI para objetivos Linux y BSD [2]. Para las plataformas que admiten este ABI y también el enlace débil, typeid()devuelve un objeto coherente y único para cada tipo, incluso a través de límites de enlace dinámico. Puede probar &typeid(a) == &typeid(b), o simplemente confiar en el hecho de que la prueba portátil en typeid(a) == typeid(b)realidad solo compara un puntero internamente.

En el ABI preferido de GCC, una clase vtable siempre tiene un puntero a una estructura RTTI por tipo, aunque podría no usarse. Por lo tanto, una typeid()llamada en sí solo debería costar tanto como cualquier otra búsqueda de vtable (lo mismo que llamar a una función miembro virtual), y el soporte RTTI no debería usar ningún espacio adicional para cada objeto.

Por lo que puedo ver, las estructuras RTTI utilizadas por GCC (estas son todas las subclases de std::type_info) solo contienen unos pocos bytes para cada tipo, aparte del nombre. No me queda claro si los nombres están presentes en el código de salida incluso con -fno-rtti. De cualquier manera, el cambio en el tamaño del binario compilado debe reflejar el cambio en el uso de la memoria en tiempo de ejecución.

Un experimento rápido (usando GCC 4.4.3 en Ubuntu 10.04 de 64 bits) muestra que en -fno-rttirealidad aumenta el tamaño binario de un programa de prueba simple en unos pocos cientos de bytes. Esto sucede consistentemente en combinaciones de -gy -O3. No estoy seguro de por qué aumentaría el tamaño; Una posibilidad es que el código STL de GCC se comporte de manera diferente sin RTTI (ya que las excepciones no funcionarán).

[1] Conocido como Itanium C ++ ABI, documentado en http://www.codesourcery.com/public/cxx-abi/abi.html . Los nombres son terriblemente confusos: el nombre se refiere a la arquitectura de desarrollo original, aunque la especificación ABI funciona en muchas arquitecturas, incluyendo i686 / x86_64. Los comentarios en la fuente interna de GCC y el código STL se refieren a Itanium como el "nuevo" ABI en contraste con el "viejo" que usaron antes. Peor aún, el "nuevo" / Itanium ABI se refiere a todas las versiones disponibles a través de -fabi-version; el "viejo" ABI es anterior a esta versión. GCC adoptó el Itanium / versionado / "nuevo" ABI en la versión 3.0; el "viejo" ABI se usó en 2.95 y anteriores, si estoy leyendo sus registros de cambios correctamente.

[2] No pude encontrar ninguna std::type_infoestabilidad de objeto de listado de recursos por plataforma. Para los compiladores que tenía acceso a, he utilizado la siguiente: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. Esta macro controla el comportamiento de operator==for std::type_infoen el STL de GCC, a partir de GCC 3.0. Encontré que mingw32-gcc obedece a la ABI de Windows C ++, donde los std::type_infoobjetos no son únicos para un tipo en las DLL; typeid(a) == typeid(b)llamadas strcmpdebajo de las sábanas. Especulo que en objetivos incrustados de un solo programa como AVR, donde no hay un código para vincular, los std::type_infoobjetos siempre son estables.

Sbrudenell
fuente
66
Las excepciones funcionan sin RTTI. (Se le permite lanzar un inty no hay vtable en eso :))
Billy ONeal
3
@Dupuplicator: Y sin embargo, cuando apago RTTI en mi compilador, funcionan bien. Lamento decepcionarte.
Billy ONeal
55
El mecanismo de manejo de excepciones debe ser capaz de funcionar con cualquier tipo de cumplimentación de algunos requisitos básicos. Usted es libre de sugerir cómo manejar las excepciones de lanzamiento y captura de tipo arbitrario a través de los límites del módulo sin RTTI. Tenga en cuenta que se requiere una conversión ascendente y descendente.
Deduplicador
15
typeid (a) == typeid (b) NO es lo mismo que B * ba = dynamic_cast <B *> (& a). Pruébelo en objetos con herencia múltiple como un nivel aleatorio en el árbol de clases derivado y encontrará que typeid () == typeid () no dará un resultado positivo. Dynamic_cast es la única forma de buscar el árbol de herencia real. Deje de pensar en los ahorros potenciales deshabilitando RTTI y simplemente utilícelo. Si está por encima de la capacidad, optimice su hinchazón de código. Intente evitar el uso de dynamic_cast dentro de los bucles internos o cualquier otro código crítico de rendimiento y estará bien.
mysticcoder
3
@mcoder Es por eso que el artículo declara explícitamente eso the latter necessarily involves traversing an inheritance tree plus comparisons. @CoryB Puedes "permitirte" hacerlo cuando no necesites soportar la transmisión desde todo el árbol de herencia. Por ejemplo, si desea encontrar todos los elementos de tipo X en una colección, pero no aquellos que se derivan de X, entonces lo que debe usar es lo primero. Si necesita encontrar también todas las instancias derivadas, deberá usar la última.
Aidiakapi
48

Quizás estas cifras ayudarían.

Estaba haciendo una prueba rápida usando esto:

  • GCC Clock () + XCode's Profiler.
  • 100,000,000 iteraciones de bucle.
  • 2 x 2.66 GHz de doble núcleo Intel Xeon.
  • La clase en cuestión se deriva de una sola clase base.
  • typeid (). name () devuelve "N12fastdelegate13FastDelegate1IivEE"

Se probaron 5 casos:

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 es solo mi código real, ya que necesitaba crear un objeto de ese tipo antes de verificar si es similar a uno que ya tengo.

Sin optimización

Para lo cual los resultados fueron (he promediado algunas ejecuciones):

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.

Entonces la conclusión sería:

  • Para casos de conversión simples sin optimización typeid()es más de dos veces más rápido que dyncamic_cast.
  • En una máquina moderna, la diferencia entre los dos es de aproximadamente 1 nanosegundo (una millonésima parte de un milisegundo).

Con optimización (-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.

Entonces la conclusión sería:

  • Para casos de conversión simples con optimización, typeid()es casi x20 más rápido que dyncamic_cast.

Gráfico

ingrese la descripción de la imagen aquí

El código

Como se solicitó en los comentarios, el código está debajo (un poco desordenado, pero funciona). 'FastDelegate.h' está disponible desde aquí .

#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
fuente
1
Por supuesto, el reparto dinámico es más general: funciona si el elemento es más derivado. Por ejemplo, class a {}; class b : public a {}; class c : public b {};cuando el objetivo es una instancia de c, funcionará bien cuando se prueba para la clase bcon dynamic_cast, pero no con la typeidsolución. Sin embargo, sigue siendo razonable, +1
Billy ONeal
34
Este punto de referencia es completamente falso con optimizaciones : la verificación de typeid es invariante en bucle y se mueve fuera del bucle. No es interesante en absoluto, es un punto de referencia básico no-no.
Vuelva a instalar a Mónica
3
@Kuba: Entonces el punto de referencia es falso. Esa no es una razón para comparar con las optimizaciones desactivadas; Esa es una razón para escribir mejores puntos de referencia.
Billy ONeal
3
una vez más, esto es un fracaso. "Para casos de conversión simples con optimización, typeid () es casi x20 más rápido que dyncamic_cast". ellos NO hacen lo mismo. Hay una razón por la que dynamic_cast es más lento.
mysticcoder
1
@KubaOber: total +1. Esto es muy clásico. y debería ser obvio por el aspecto del número de ciclos que esto sucedió.
v.oddou
38

Depende de la escala de las cosas. En su mayor parte, son solo un par de verificaciones y algunas desreferencias de puntero. En la mayoría de las implementaciones, en la parte superior de cada objeto que tiene funciones virtuales, hay un puntero a una tabla virtual que contiene una lista de punteros a todas las implementaciones de la función virtual en esa clase. Supongo que la mayoría de las implementaciones usarían esto para almacenar otro puntero a la estructura type_info para la clase.

Por ejemplo 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 general, el argumento real contra RTTI es la imposibilidad de mantener el código en todas partes cada vez que agrega una nueva clase derivada. En lugar de cambiar las declaraciones en todas partes, factorizarlas en funciones virtuales. Esto mueve todo el código que es diferente entre las clases a las clases mismas, de modo que una nueva derivación solo necesita anular todas las funciones virtuales para convertirse en una clase completamente funcional. Si alguna vez ha tenido que buscar en una base de código grande cada vez que alguien verifica el tipo de clase y hace algo diferente, rápidamente aprenderá a mantenerse alejado de ese estilo de programación.

Si su compilador le permite desactivar totalmente RTTI, el ahorro de tamaño de código resultante final puede ser significativo, con un espacio RAM tan pequeño. El compilador necesita generar una estructura type_info para cada clase individual con una función virtual. Si desactiva RTTI, no es necesario incluir todas estas estructuras en la imagen ejecutable.

Eclipse
fuente
44
+1 por explicar realmente por qué usar RTTI se considera una mala decisión de diseño, eso no me quedaba claro antes.
aguazales
66
Esta respuesta es una comprensión de bajo nivel del poder de C ++. "En general" y "En la mayoría de las implementaciones", usado libremente, significa que no está pensando en cómo usar bien las funciones de los idiomas. Las funciones virtuales y la reimplementación de RTTI no son la respuesta. RTTI es la respuesta. A veces solo quieres saber si un objeto es de cierto tipo. ¡Por eso está ahí! Entonces pierde unos pocos KB de RAM en algunas estructuras type_info. Gee ...
mysticcoder
16

Bueno, el perfilador nunca miente.

Como tengo una jerarquía bastante estable de 18-20 tipos que no está cambiando mucho, me preguntaba si solo usar un simple miembro enumerado haría el truco y evitaría el supuesto costo "alto" de RTTI. Era escéptico si RTTI era de hecho más costoso que solo la ifdeclaración que presenta. Chico, oh chico, ¿es así?

Resulta que RTTI es costoso, mucho más costoso que una ifdeclaración equivalente o una simple switchen una variable primitiva en C ++. Así que la respuesta de S. Lott no es completamente correcto, no es un costo adicional para el RTTI, y es no debido a simplemente tener una ifdeclaración en la mezcla. Es debido a que RTTI es muy costoso.

Esta prueba se realizó en el compilador Apple LLVM 5.0, con las optimizaciones de stock activadas (configuración del modo de lanzamiento predeterminado).

Entonces, tengo debajo de 2 funciones, cada una de las cuales descubre el tipo concreto de un objeto a través de 1) RTTI o 2) un interruptor simple. Lo hace 50,000,000 de veces. Sin más preámbulos, les presento los tiempos de ejecución relativos para 50,000,000 de carreras.

ingrese la descripción de la imagen aquí

Así es, dynamicCaststomaron el 94% del tiempo de ejecución. Mientras que el regularSwitchbloque solo tomó 3.3% .

En pocas palabras: si puede permitirse la energía para conectar y enumescribir como lo hice a continuación, probablemente lo recomendaría, si necesita hacer RTTI y el rendimiento es primordial. Solo toma configurar el miembro una vez (asegúrese de obtenerlo a través de todos los constructores ), y asegúrese de nunca escribirlo después.

Dicho esto, hacer esto no debería estropear sus prácticas de OOP ... solo debe usarse cuando la información de tipo simplemente no está disponible y se encuentra acorralado en el uso de 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
fuente
13

La forma estándar:

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

RTTI estándar es costoso porque se basa en hacer una comparación de cadena subyacente y, por lo tanto, la velocidad de RTTI puede variar según la longitud del nombre de la clase.

La razón por la que se utilizan las comparaciones de cadenas es para que funcione de manera coherente entre los límites de la biblioteca / DLL. Si construye su aplicación de forma estática y / o está utilizando ciertos compiladores, entonces probablemente pueda usar:

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

Lo cual no garantiza que funcione (nunca dará un falso positivo, pero puede dar falsos negativos) pero puede ser hasta 15 veces más rápido. Esto se basa en la implementación de typeid () para trabajar de cierta manera y todo lo que está haciendo es comparar un puntero de caracteres interno. Esto también es a veces equivalente a:

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

Usted puede sin embargo utilizar un híbrido con seguridad que será muy rápido si los tipos coinciden, y será peor de los casos para los tipos no coincidentes:

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

Para comprender si necesita optimizar esto, necesita ver cuánto tiempo dedica a obtener un nuevo paquete, en comparación con el tiempo que lleva procesar el paquete. En la mayoría de los casos, una comparación de cadenas probablemente no será una gran sobrecarga. (dependiendo de su clase o espacio de nombres :: longitud del nombre de la clase)

La forma más segura de optimizar esto es implementar su propio typeid como int (o enum Type: int) como parte de su clase Base y usarlo para determinar el tipo de la clase, y luego usar static_cast <> o reinterpret_cast < >

Para mí, la diferencia es aproximadamente 15 veces en MS VS 2005 C ++ SP1 no optimizado.

Mario
fuente
2
"El RTTI estándar es costoso porque se basa en hacer una comparación de cadena subyacente" - no, no hay nada "Estándar" sobre esto; es sólo la forma en que su aplicación de typeid::operatortrabajo s . GCC en una plataforma compatible, por ejemplo, ya utiliza comparaciones de char *s, sin que nosotros lo obliguemos : gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/… . Claro, su forma hace que MSVC se comporte mucho mejor que el predeterminado en su plataforma, así que felicitaciones, y no sé cuáles son los "algunos objetivos" que usan punteros de forma nativa ... pero mi punto es que el comportamiento de MSVC no es de ninguna manera "Estándar".
underscore_d
7

Para una verificación simple, RTTI puede ser tan barato como una comparación de puntero. Para la verificación de herencia, puede ser tan costoso como strcmppara cada tipo en un árbol de herencia si está dynamic_casthaciendo un recorrido desde arriba hacia abajo en una implementación.

También puede reducir la sobrecarga al no usar dynamic_casty, en su lugar, verificar el tipo explícitamente mediante & typeid (...) == & typeid (type). Si bien eso no necesariamente funciona para .dlls u otro código cargado dinámicamente, puede ser bastante rápido para cosas que están vinculadas estáticamente.

Aunque en ese momento es como usar una declaración de cambio, así que ahí lo tienes.

MSN
fuente
1
¿Tiene alguna referencia para la versión strcmp? Parece extremadamente ineficiente e inexacto usar strcmp para una verificación de tipo.
JaredPar
En una implementación deficiente que podría tener múltiples objetos type_info por tipo, podría implementar bool type_info :: operator == (const type_info & x) const como "! Strcmp (name (), x.name ())"
Greg Rogers,
3
Ingrese al desmontaje de dynamic_cast o typeid (). Operator == para MSVC y encontrará un strcmp allí. Supongo que está ahí para el horrible caso en el que se compara con un tipo compilado en otro .dll. Y usa el nombre destrozado, por lo que al menos es correcto dado el mismo compilador.
MSN
1
se supone que debes hacer "typeid (...) == typeid (type)" y no comparar la dirección
Johannes Schaub - litb
1
Mi punto es que puede hacer & typeid (...) == & typeid (blah) como salida anticipada y estará seguro. Puede que en realidad no haga nada útil ya que typeid (...) podría generarse en la pila, pero si sus direcciones son iguales, entonces sus tipos son iguales.
MSN
6

Siempre es mejor medir cosas. En el siguiente código, en g ++, el uso de la identificación de tipo codificada a mano parece ser aproximadamente tres veces más rápido que RTTI. Estoy seguro de que una implementación codificada a mano más realista usando cadenas en lugar de caracteres sería más lenta, lo que acercaría los tiempos.

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

fuente
1
intenta no hacerlo con dynamic_cast, sino con typeid. Podría acelerar el rendimiento.
Johannes Schaub - litb
1
pero utilizando dynamic_cast es más realista, al menos mirando mi código
2
hace algo diferente: comprueba también si bp apunta a un tipo derivado de A. su == 'A' verifica si apunta exactamente a una 'A'. También creo que la prueba es un tanto injusta: el compilador puede ver fácilmente que bp no puede apuntar a nada diferente a A. pero creo que no se optimiza aquí.
Johannes Schaub - litb
De todos modos, he probado tu código. y me da "0.016s" para RTTI y "0.044s" para las llamadas a funciones virtuales. (usando -O2)
Johannes Schaub - litb
aunque cambiarlo para usar typeid no hace ninguna diferencia aquí (todavía 0.016s)
Johannes Schaub - litb
4

Hace un tiempo medí los costos de tiempo para RTTI en los casos específicos de MSVC y GCC para un PowerPC de 3 ghz. En las pruebas que ejecuté (una aplicación C ++ bastante grande con un árbol de clase profunda), cada una dynamic_cast<>cuesta entre 0.8 μs y 2 μs, dependiendo de si golpeó o falló.

Crashworks
fuente
2

Entonces, ¿qué tan caro es RTTI?

Eso depende completamente del compilador que estés usando. Entiendo que algunos usan comparaciones de cadenas y otros usan algoritmos reales.

Su única esperanza es escribir un programa de muestra y ver qué hace su compilador (o al menos determinar cuánto tiempo lleva ejecutar un millón dynamic_castso un millón typeids).

Max Lybbert
fuente
1

RTTI puede ser barato y no necesita necesariamente un strcmp. El compilador limita la prueba para realizar la jerarquía real, en orden inverso. Entonces, si tiene una clase C que es un hijo de la clase B que es un hijo de la clase A, la transmisión dinámica de un A * ptr a un C * ptr implica solo una comparación de puntero y no dos (BTW, solo el puntero de tabla vptr es comparado). La prueba es como "if (vptr_of_obj == vptr_of_C) return (C *) obj"

Otro ejemplo, si tratamos de hacer dynamic_cast de A * a B *. En ese caso, el compilador verificará ambos casos (obj es una C y obj es una B) por turnos. Esto también se puede simplificar a una sola prueba (la mayoría de las veces), ya que la tabla de funciones virtuales se realiza como una agregación, por lo que la prueba se reanuda 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

El diseño de la memoria de

vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ]

¿Cómo sabe el compilador para optimizar esto en tiempo de compilación?

En el momento de la compilación, el compilador conoce la jerarquía actual de objetos, por lo que se niega a compilar diferentes tipos de jerarquía dynamic_casting. Luego solo tiene que manejar la profundidad de la jerarquía y agregar la cantidad invertida de pruebas para que coincida con dicha profundidad.

Por ejemplo, esto no compila:

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

RTTI puede ser "costoso" porque ha agregado una declaración if cada vez que realiza la comparación RTTI. En iteraciones profundamente anidadas, esto puede ser costoso. En algo que nunca se ejecuta en un bucle, es esencialmente gratuito.

La opción es usar un diseño polimórfico adecuado, eliminando la declaración if. En bucles profundamente anidados, esto es esencial para el rendimiento. De lo contrario, no importa mucho.

RTTI también es costoso porque puede oscurecer la jerarquía de la subclase (si es que hay alguna). Puede tener el efecto secundario de eliminar el "orientado a objetos" de la "programación orientada a objetos".

S.Lott
fuente
2
No necesariamente: iba a usarlo indirectamente a través de dynamic_cast y mantener la jerarquía en su lugar, porque necesito downcast porque cada subtipo necesita tener datos diferentes (de tamaño variable) que deben aplicarse de manera diferente, por lo tanto, dynamic_cast.
Cristián Romo
1
@ Cristián Romo: Actualice su pregunta con estos nuevos hechos. dynamic_cast es un mal (a veces) necesario en C ++. Preguntar sobre el rendimiento de RTTI cuando se ve obligado a hacerlo no tiene mucho sentido.
S.Lott
@ S.Lott: actualizado. Perdón por la confusión.
Cristián Romo
1
Hice un experimento sobre esto en este momento: resulta que RTTI es significativamente más costoso que la ifdeclaración que presenta cuando verifica la información del tipo de tiempo de ejecución de esta manera.
bobobobo