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_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 así permitirles actuar de manera completamente diferente.
Según tengo entendido, dynamic_cast
usa RTTI, así que me preguntaba qué tan factible sería usarlo en un sistema limitado.
fuente
dynamic_cast
en 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.Respuestas:
Independientemente del compilador, siempre puede ahorrar en tiempo de ejecución si puede permitirse hacer
en vez de
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 entypeid(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-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é 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_info
estabilidad 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 deoperator==
forstd::type_info
en el STL de GCC, a partir de GCC 3.0. Encontré que mingw32-gcc obedece a la ABI de Windows C ++, donde losstd::type_info
objetos no son únicos para un tipo en las DLL;typeid(a) == typeid(b)
llamadasstrcmp
debajo de las sábanas. Especulo que en objetivos incrustados de un solo programa como AVR, donde no hay un código para vincular, losstd::type_info
objetos siempre son estables.fuente
int
y no hay vtable en 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.Quizás estas cifras ayudarían.
Estaba haciendo una prueba rápida usando esto:
Se probaron 5 casos:
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):
Entonces la conclusión sería:
typeid()
es más de dos veces más rápido quedyncamic_cast
.Con optimización (-Os)
Entonces la conclusión sería:
typeid()
es casi x20 más rápido quedyncamic_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 aquí .
fuente
class a {}; class b : public a {}; class c : public b {};
cuando el objetivo es una instancia dec
, funcionará bien cuando se prueba para la claseb
condynamic_cast
, pero no con latypeid
solución. Sin embargo, sigue siendo razonable, +1Depende 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 ++:
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.
fuente
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
if
declaración que presenta. Chico, oh chico, ¿es así?Resulta que RTTI es costoso, mucho más costoso que una
if
declaración equivalente o una simpleswitch
en 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 unaif
declaració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.
Así es,
dynamicCasts
tomaron el 94% del tiempo de ejecución. Mientras que elregularSwitch
bloque solo tomó 3.3% .En pocas palabras: si puede permitirse la energía para conectar y
enum
escribir 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.
fuente
La forma estándar:
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:
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:
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:
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.
fuente
typeid::operator
trabajo s . GCC en una plataforma compatible, por ejemplo, ya utiliza comparaciones dechar *
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".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
strcmp
para cada tipo en un árbol de herencia si estádynamic_cast
haciendo un recorrido 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 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.
fuente
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.
fuente
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ó.fuente
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_casts
o un millóntypeid
s).fuente
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
¿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:
fuente
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".
fuente
if
declaración que presenta cuando verifica la información del tipo de tiempo de ejecución de esta manera.