resueltos - ¿Qué puede hacer que C++ RTTI sea indeseable?
manual completo de c++ pdf (4)
Hay varias razones por las que LLVM lanza su propio sistema RTTI. Este sistema es simple y poderoso, y se describe en una sección del Manual del programador de LLVM . Como ha señalado otro afiche, las Normas de codificación plantean dos problemas principales con C ++ RTTI: 1) el costo del espacio y 2) el bajo rendimiento de su uso.
El costo de espacio de RTTI es bastante alto: cada clase con un vtable (al menos un método virtual) obtiene información RTTI, que incluye el nombre de la clase y la información sobre sus clases base. Esta información se usa para implementar el operador typeid así como dynamic_cast . Debido a que este costo se paga por cada clase con un vtable (y no, las optimizaciones de PGO y tiempo de enlace no ayudan, porque el vtable apunta a la información RTTI) LLVM crea con -fno-rtti. Empíricamente, esto ahorra del orden del 5-10% del tamaño del ejecutable, que es bastante sustancial. LLVM no necesita un equivalente de typeid, por lo que mantener los nombres (entre otras cosas en type_info) para cada clase es solo una pérdida de espacio.
El bajo rendimiento es bastante fácil de ver si hace algún benchmarking o mira el código generado para operaciones simples. El operador LLVM isa <> generalmente se compila en una sola carga y una comparación con una constante (aunque las clases controlan esto en función de cómo implementan su método classof). Aquí hay un ejemplo trivial:
#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return isa<ConstantInt>(V); }
Esto compila a:
$ clang t.cc -S -o - -O3 -I$HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer ... __Z13isConstantIntPN4llvm5ValueE: cmpb $9, 8(%rdi) sete %al movzbl %al, %eax ret
que (si no lees ensamblaje) es una carga y se compara con una constante. Por el contrario, el equivalente con dynamic_cast es:
#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return dynamic_cast<ConstantInt*>(V) != 0; }
que se compila a:
clang t.cc -S -o - -O3 -I$HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer ... __Z13isConstantIntPN4llvm5ValueE: pushq %rax xorb %al, %al testq %rdi, %rdi je LBB0_2 xorl %esi, %esi movq $-1, %rcx xorl %edx, %edx callq ___dynamic_cast testq %rax, %rax setne %al LBB0_2: movzbl %al, %eax popq %rdx ret
Esto es mucho más código, pero el asesino es el llamado a __dynamic_cast, que luego tiene que arrastrarse a través de las estructuras de datos RTTI y hacer un recorrido muy general, dinámicamente calculado a través de este material. Esto es varios órdenes de magnitud más lento que una carga y comparar.
Ok, vale, entonces es más lento, ¿por qué importa esto? Esto es importante porque LLVM hace MUCHAS pruebas de tipo. Muchas partes de los optimizadores se basan en patrones que coinciden con construcciones específicas en el código y realizan sustituciones en ellos. Por ejemplo, aquí hay un código para hacer coincidir un patrón simple (que ya sabe que Op0 / Op1 son el lado izquierdo y derecho de una operación de resta entera):
// (X*2) - X -> X
if (match(Op0, m_Mul(m_Specific(Op1), m_ConstantInt<2>())))
return Op1;
El operador de coincidencia y m_ * son metaprogramas de plantilla que se reducen a una serie de llamadas isa / dyn_cast, cada una de las cuales tiene que hacer una comprobación de tipo. El uso de dynamic_cast para este tipo de emparejamiento de patrones finos sería brutal y notablemente lento.
Finalmente, hay otro punto, que es uno de expresividad. Los diferentes operadores ''rtti'' que utiliza LLVM se utilizan para expresar cosas diferentes: comprobación de tipo, emisión dinámica, conversión forzada (afirmativa), manipulación nula, etc. La emisión dinámica de C ++ no ofrece (nativamente) ninguna de estas funciones.
Al final, hay dos formas de ver esta situación. En el lado negativo, C ++ RTTI está demasiado estrechamente definido para lo que mucha gente quiere (reflexión total) y es demasiado lento para ser útil incluso para cosas simples como lo que hace LLVM. En el lado positivo, el lenguaje C ++ es lo suficientemente poderoso como para que podamos definir abstracciones como esta como código de biblioteca y optar por no utilizar la función de idioma. Una de mis cosas favoritas sobre C ++ es qué tan poderosas y elegantes pueden ser las bibliotecas. ¡RTTI ni siquiera es muy alto entre mis características menos favoritas de C ++ :)!
-Chris
En cuanto a la documentación de LLVM, mencionan que utilizan "una forma personalizada de RTTI" , y esta es la razón por la que tienen las funciones de plantilla isa<>
, cast<>
y dyn_cast<>
.
Por lo general, leer que una biblioteca vuelve a implementar algunas funcionalidades básicas de un idioma es un olor a código terrible y solo invita a correr. Sin embargo, estamos hablando de LLVM: los chicos están trabajando en un compilador C ++ y un tiempo de ejecución C ++. Si no saben lo que están haciendo, estoy bastante jodido porque prefiero el clang
a la versión de gcc
que viene con Mac OS.
Aún así, al ser menos experimentados que ellos, me pregunto cuáles son los peligros del RTTI normal. Sé que funciona solo para los tipos que tienen una tabla v, pero eso solo genera dos preguntas:
- Como solo necesitas un método virtual para tener un vtable, ¿por qué no solo marcan un método como
virtual
? Los destructores virtuales parecen ser buenos en esto. - Si su solución no usa RTTI regular, ¿alguna idea de cómo se implementó?
La razón predominante es que tienen dificultades para mantener el uso de la memoria lo más bajo posible.
RTTI solo está disponible para las clases que cuentan con al menos un método virtual, lo que significa que las instancias de la clase contendrán un puntero a la tabla virtual.
En una arquitectura de 64 bits (que es común en la actualidad), un único puntero es de 8 bytes. Dado que el compilador crea una instancia de muchos objetos pequeños, esto se acumula bastante rápido.
Por lo tanto, existe un esfuerzo continuo para eliminar las funciones virtuales tanto como sea posible (y práctico) e implementar lo que habrían sido funciones virtuales con la instrucción de switch
, que tiene una velocidad de ejecución similar pero un impacto de memoria significativamente menor.
Su preocupación constante por el consumo de memoria ha valido la pena, ya que Clang consume mucha menos memoria que gcc, por ejemplo, lo cual es importante cuando se ofrece la biblioteca a los clientes.
Por otro lado, también significa que agregar un nuevo tipo de nodo generalmente da como resultado la edición de código en una buena cantidad de archivos porque cada switch debe adaptarse (afortunadamente, los compiladores emiten advertencias si se salta un miembro enumerado en un switch). Así que aceptaron hacer el mantenimiento un poco más difícil en nombre de la eficiencia de la memoria.
Los estándares de codificación LLVM parecen responder esta pregunta bastante bien:
En un esfuerzo por reducir el código y el tamaño del ejecutable, LLVM no utiliza RTTI (por ejemplo, dynamic_cast <>) o excepciones. Estas dos características de idioma violan el principio general de C ++ de "solo pagas por lo que usas", lo que causa una acumulación ejecutable incluso si las excepciones nunca se usan en la base de códigos, o si RTTI nunca se usa para una clase. Debido a esto, los desactivamos globalmente en el código.
Dicho esto, LLVM hace un uso extensivo de una forma de RTTI enrollada a mano que usa plantillas como isa <>, cast <>, y dyn_cast <>. Esta forma de RTTI es opcional y se puede agregar a cualquier clase. También es sustancialmente más eficiente que dynamic_cast <>.
Aquí hay un excelente artículo sobre RTTI y por qué es posible que necesites rodar tu propia versión del mismo.
No soy un experto en el C ++ RTTI, pero también he implementado mi propio RTTI porque definitivamente hay razones por las que necesitarías hacer eso. En primer lugar, el sistema RTTI de C ++ no es muy rico en funciones, básicamente todo lo que puede hacer es escribir y obtener información básica. ¿Qué pasa si, en tiempo de ejecución, tiene una cadena con el nombre de una clase, y desea construir un objeto de esa clase, buena suerte haciendo esto con C ++ RTTI. Además, C ++ RTTI no es realmente (o fácilmente) portátil entre los módulos (no se puede identificar la clase de un objeto que se creó a partir de otro módulo (dll / soo exe). Del mismo modo, la implementación de C ++ RTTI es específica del compilador y Por lo general, es costoso activarlo en términos de sobrecarga adicional para implementarlo en todos los tipos. Finalmente, no es realmente persistente, por lo que no se puede usar para guardar / cargar archivos por ejemplo (por ejemplo, puede querer guardar el datos de un objeto a un archivo, pero también le gustaría guardar el "typeid" de su clase, de modo que, en el momento de la carga, sepa qué objeto crear para cargar estos datos, lo que no se puede hacer de manera confiable con C ++ RTTI). Por todas o algunas de estas razones, muchos frameworks tienen su propio RTTI (de muy simple a muy rico en funciones). Ejemplos son wxWidget, LLVM, Boost.Serialization, etc. Esto realmente no es tan infrecuente.
Como solo necesitas un método virtual para tener un vtable, ¿por qué no solo marcan un método como virtual? Los destructores virtuales parecen ser buenos en esto.
Eso es probablemente lo que su sistema RTTI también usa. Las funciones virtuales son la base de la vinculación dinámica (vinculación en tiempo de ejecución) y, por lo tanto, es básicamente necesaria para realizar cualquier clase de identificación / información de tipo de tiempo de ejecución (no solo requerida por el RTTI de C ++, pero cualquier implementación de RTTI tendrá confiar en llamadas virtuales de una forma u otra).
Si su solución no usa RTTI regular, ¿alguna idea de cómo se implementó?
Claro, puedes buscar implementaciones de RTTI en C ++. He hecho lo mío y también hay muchas bibliotecas que tienen su propio RTTI. Es bastante simple de escribir, realmente. Básicamente, todo lo que necesita es un medio para representar de forma única un tipo (es decir, el nombre de la clase, o alguna versión modificada de él, o incluso una ID única para cada clase), algún tipo de estructura análoga a type_info
que contiene toda la información sobre el tipo que necesita, entonces necesita una función virtual "oculta" en cada clase que devolverá este tipo de información a petición (si esta función se reemplaza en cada clase derivada, funcionará). Hay, por supuesto, algunas cosas adicionales que se pueden hacer, como un repositorio único de todos los tipos, tal vez con funciones de fábrica asociadas (esto puede ser útil para crear objetos de un tipo cuando todo lo que se conoce en tiempo de ejecución es el nombre del tipo, como una cadena o el tipo ID). Además, es posible que desee agregar algunas funciones virtuales para permitir la conversión de tipo dinámico (generalmente esto se realiza llamando a la función de static_cast
de la clase más derivada y realizando static_cast
hasta el tipo al que desea static_cast
).