c++ performance rtti

c++ - ¿Qué tan caro es RTTI?



performance (11)

Entiendo que hay un recurso afectado por el uso de RTTI, pero ¿qué tan grande es? En todas partes que he visto solo dice que "RTTI es caro", pero ninguno de ellos realmente da ningún punto de referencia o datos cuantitativos que regulen la memoria, el tiempo del procesador o la velocidad.

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

Editar: Según la respuesta de S. Lott , sería mejor si incluyo lo que estoy haciendo en realidad. Estoy usando una clase para pasar datos de diferentes longitudes y que pueden realizar diferentes acciones , por lo que sería difícil hacerlo solo con funciones virtuales. Parece que el uso de unos pocos dynamic_cast s podría remediar este problema al permitir que las diferentes clases derivadas pasen a través de los diferentes niveles y aún les permita actuar de forma completamente diferente.

Según tengo entendido, dynamic_cast usa RTTI, por lo que me preguntaba qué tan factible sería usarlo en un sistema limitado.


Entonces, ¿qué tan caro es RTTI?

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

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


Bueno, el perfilador nunca miente.

Dado que tengo una jerarquía bastante estable de 18-20 tipos que no está cambiando mucho, me pregunté si el simple hecho de usar un miembro enumerado simple haría el truco y evitaría el supuestamente alto costo de RTTI. Yo era escéptico si RTTI era de hecho más costoso que solo la declaración if que presenta. Chico oh chico, ¿verdad?

Resulta que RTTI es costoso, mucho más costoso que una declaración if equivalente o un simple switch en una variable primitiva en C ++. Entonces la respuesta de S.Lott no es completamente correcta, hay un costo adicional para RTTI, y no se debe solo a tener una declaración if en la mezcla. Es debido a que RTTI es muy caro.

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

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

Así es, el dynamicCasts tomó el 94% del tiempo de ejecución. Mientras que el bloque regularSwitch solo tomó 3.3% .

Para resumir: si puede permitirse la energía para enganchar un tipo de enum como lo hice a continuación, probablemente lo recomendaría, si necesita hacer RTTI y el rendimiento es primordial. Solo se necesita 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 POO ... solo debe usarse cuando la información de tipo simplemente no está disponible y se ve arrinconado al usar 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 ) ; }


Depende de la escala de las cosas. En su mayor parte, solo son un par de cheques y algunas referencias al puntero. En la mayoría de las implementaciones, en la parte superior de cada objeto que tiene funciones virtuales, hay un puntero a un vtable 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 la necesidad de modificar el código en todas partes cada vez que agrega una nueva clase derivada. En lugar de cambiar 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 tiene que anular todas las funciones virtuales para convertirse en una clase totalmente funcional. Si alguna vez ha tenido que buscar en una gran base de códigos por cada vez que alguien verifica el tipo de una clase y hace algo diferente, aprenderá rápidamente a mantenerse alejado de ese estilo de programación.

Si su compilador le permite apagar completamente RTTI, el ahorro resultante en el tamaño del código resultante puede ser significativo, con un espacio RAM tan pequeño. El compilador necesita generar una estructura type_info para cada clase con una función virtual. Si apaga RTTI, no es necesario que todas estas estructuras estén incluidas en la imagen ejecutable.


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 bastante grande de C ++ con un árbol de clase profunda), cada dynamic_cast<> cuesta entre 0.8μs y 2μs, dependiendo de si golpea o no.


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

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

en lugar de

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

El primero implica solo una comparación de std::type_info ; el ú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 que el remitente debe evitar RTTI por motivos de diseño. Sin embargo, hay buenas razones para usar RTTI (principalmente debido a boost :: any). Teniendo esto en cuenta, es útil conocer su uso real de recursos en implementaciones comunes.

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

tl; dr: RTTI en GCC usa 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 sabes que siempre estarás en una plataforma bendecida, RTTI está muy cerca de ser gratis.

Detalles arenosos:

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

En la ABI preferida de GCC, una clase vtable siempre contiene un puntero a una estructura RTTI por tipo, aunque podría no ser utilizada. Por lo tanto, una llamada typeid() solo debería costar tanto como cualquier otra búsqueda 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, además del nombre. No tengo 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 de tiempo de ejecución.

Un experimento rápido (usando GCC 4.4.3 en Ubuntu 10.04 de 64 bits) muestra que -fno-rtti realidad aumenta el tamaño binario de un programa de prueba simple en unos pocos cientos de bytes. Esto sucede consistentemente en combinaciones de -g y -O3 . No estoy seguro de por qué el tamaño aumentaría; 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 "antiguo" ABI es anterior a este control de versiones. GCC adoptó Itanium / versioned / "new" ABI en la versión 3.0; el "viejo" ABI se usó en 2.95 y anteriormente, si estoy leyendo sus registros de cambios correctamente.

[2] No pude encontrar ningún recurso que enumere la estabilidad del objeto std::type_info por plataforma. Para los compiladores a los que tuve acceso, utilicé lo siguiente: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES . Esta macro controla el comportamiento del operator== para std::type_info en el STL de GCC, a partir de GCC 3.0. Encontré que mingw32-gcc obedece a Windows C ++ ABI, donde los objetos std::type_info no son únicos para un tipo en DLL; typeid(a) == typeid(b) llama a strcmp debajo de las cubiertas. Yo especulo que en los objetivos incrustados de un solo programa como AVR, donde no hay ningún código para vincular, los objetos std::type_info son siempre estables.


La forma estándar:

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

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

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

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

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

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

Sin embargo, puede utilizar un híbrido de forma segura, que será muy rápido si los tipos coinciden, y será el peor caso para los tipos no coincidentes:

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

Para saber si necesita optimizar esto, necesita ver cuánto tiempo pasa gastando un paquete nuevo, 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 tu clase o espacio de nombre :: 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 simplemente use static_cast <> o reinterpret_cast < >

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


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

También puede reducir la sobrecarga al no usar dynamic_cast y, en su lugar, verificar el tipo explícitamente a través de & typeid (...) == y typeid (tipo). 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 punto es como usar una declaración de cambio, entonces ahí lo tienes.


Quizás estas figuras 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 única clase base.
  • typeid (). name () devuelve "N12fastdelegate13FastDelegate1IivEE"

5 Casos fueron probados:

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 los cuales fueron los resultados (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 simples de typeid() 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 de 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 typeid() simples con optimización, typeid() es casi x20 más rápido que dyncamic_cast .

Gráfico

El código

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

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


RTTI puede ser "caro" porque ha agregado un enunciado if cada vez que hace 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 elección es usar un diseño polimórfico adecuado, eliminando el enunciado if. En los 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 subclase (si es que hay una). Puede tener el efecto secundario de eliminar el "objeto orientado" de la "programación orientada a objetos".


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 hija de la clase B, que es hija de la clase A, dynamic_cast de A * ptr a C * ptr implica solo una comparación de puntero y no dos (por cierto, solo el puntero de la tabla vptr es comparado). La prueba es como "if (vptr_of_obj == vptr_of_C) return (C *) obj"

Otro ejemplo, si tratamos de dynamic_cast de A * a B *. En ese caso, el compilador verificará ambos casos (siendo obj un C y obj un B) por turnos. Esto también se puede simplificar a una única 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 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 tiempo de compilación, el compilador conoce la jerarquía actual de los objetos, por lo que se niega a compilar diferentes jerarquías de tipos 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);


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

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