web-dev-qa-db-fra.com

Qu'est-ce qui peut rendre C ++ RTTI indésirable à utiliser?

En regardant la documentation LLVM, ils mentionnent que ils utilisent "une forme personnalisée de RTTI" , et c'est la raison pour laquelle ils ont isa<>, cast<> et dyn_cast<> fonctions modèles.

Habituellement, lire qu'une bibliothèque réimplémente certaines fonctionnalités de base d'un langage est une odeur de code terrible et invite simplement à s'exécuter. Cependant, c'est de LLVM dont nous parlons: les gars travaillent sur un compilateur C++ et un runtime C++. S'ils ne savent pas ce qu'ils font, je suis plutôt foutu parce que je préfère clang à la version gcc fournie avec Mac OS.

Pourtant, étant moins expérimenté qu'eux, je me demande quels sont les pièges du RTTI normal. Je sais que cela ne fonctionne que pour les types qui ont une v-table, mais cela ne soulève que deux questions:

  • Puisque vous avez juste besoin d'une méthode virtuelle pour avoir une table virtuelle, pourquoi ne marque-t-on pas simplement une méthode comme virtual? Les destructeurs virtuels semblent être bons dans ce domaine.
  • Si leur solution n'utilise pas de RTTI ordinaire, une idée de la façon dont elle a été mise en œuvre?
67
zneak

Il y a plusieurs raisons pour lesquelles LLVM lance son propre système RTTI. Ce système est simple et puissant, et décrit dans une section de le manuel du programmeur LLVM . Comme une autre affiche l'a souligné, le Coding Standards pose deux problèmes majeurs avec C++ RTTI: 1) le coût de l'espace et 2) les mauvaises performances de son utilisation.

Le coût d'espace de RTTI est assez élevé: chaque classe avec une vtable (au moins une méthode virtuelle) obtient des informations RTTI, qui incluent le nom de la classe et des informations sur ses classes de base. Ces informations sont utilisées pour implémenter l'opérateur typeid ainsi que dynamic_cast . Parce que ce coût est payé pour chaque classe avec une vtable (et non, les optimisations PGO et de temps de liaison ne sont pas utiles, car la vtable pointe vers les informations RTTI) LLVM construit avec -fno-rtti. Empiriquement, cela économise de l'ordre de 5 à 10% de la taille de l'exécutable, ce qui est assez substantiel. LLVM n'a pas besoin d'un équivalent de typeid, donc garder les noms (entre autres dans type_info) pour chaque classe est juste une perte d'espace.

Les mauvaises performances sont assez faciles à voir si vous effectuez des analyses comparatives ou regardez le code généré pour des opérations simples. L'opérateur LLVM isa <> compile généralement en une seule charge et une comparaison avec une constante (bien que les classes contrôlent cela en fonction de la façon dont elles implémentent leur méthode classof). Voici un exemple trivial:

#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return isa<ConstantInt>(V); }

Cela compile pour:

 $ 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 
 Ret

qui (si vous ne lisez pas Assembly) est une charge et comparez avec une constante. En revanche, l'équivalent avec dynamic_cast est:

#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return dynamic_cast<ConstantInt*>(V) != 0; }

qui se résume à:

 clang t.cc -S -o - -O3 -I $ HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer 
 ... 
 __ Z13isConstantIntPal4VElVV5 : 
 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 

C'est beaucoup plus de code, mais le tueur est l'appel à __dynamic_cast, qui doit ensuite parcourir les structures de données RTTI et faire une marche très générale et calculée dynamiquement à travers ce genre de choses. C'est plusieurs ordres de grandeur plus lent qu'une charge et comparez.

Ok, ok, donc c'est plus lent, pourquoi est-ce important? Cela est important car LLVM effectue BEAUCOUP de vérifications de type. De nombreuses parties des optimiseurs sont construites autour de modèles correspondant à des constructions spécifiques dans le code et effectuant des substitutions sur celles-ci. Par exemple, voici un code pour faire correspondre un modèle simple (qui sait déjà que Op0/Op1 sont les côtés gauche et droit d'une opération de soustraction d'entier):

  // (X*2) - X -> X
  if (match(Op0, m_Mul(m_Specific(Op1), m_ConstantInt<2>())))
    return Op1;

L'opérateur de correspondance et m_ * sont des métaprogrammes de modèle qui se résument à une série d'appels isa/dyn_cast, chacun devant effectuer une vérification de type. L'utilisation de dynamic_cast pour ce type de correspondance de motifs fins serait brutalement et incroyablement lente.

Enfin, il y a un autre point, celui de l'expressivité. Les différents opérateurs 'rtti' que LLVM utilise sont utilisés pour exprimer différentes choses: vérification de type, dynamic_cast, transtypage forcé (affirmation), gestion des valeurs nulles, etc. Fonctionnalité.

Au final, il y a deux façons de voir cette situation. Du côté négatif, C++ RTTI est à la fois trop étroitement défini pour ce que beaucoup de gens veulent (réflexion complète) et est trop lent pour être utile même pour des choses simples comme ce que fait LLVM. Du côté positif, le langage C++ est suffisamment puissant pour que nous puissions définir des abstractions comme celle-ci comme code de bibliothèque et désactiver l'utilisation de la fonction de langage. L'une de mes choses préférées à propos de C++ est la puissance et l'élégance des bibliothèques. RTTI n'est même pas très élevé parmi mes fonctionnalités les moins préférées de C++ :)!

-Chris

80
Chris Lattner

Les normes de codage LLVM semblent répondre assez bien à cette question:

Afin de réduire le code et la taille de l'exécutable, LLVM n'utilise pas RTTI (par exemple dynamic_cast <>) ni d'exceptions. Ces deux fonctionnalités de langage violent le principe général C++ de "vous ne payez que pour ce que vous utilisez", provoquant un ballonnement exécutable même si des exceptions ne sont jamais utilisées dans la base de code, ou si RTTI n'est jamais utilisé pour une classe. Pour cette raison, nous les désactivons globalement dans le code.

Cela dit, LLVM utilise largement une forme RTTI roulée à la main qui utilise des modèles tels que isa <>, cast <> et dyn_cast <>. Cette forme de RTTI est opt-in et peut être ajoutée à n'importe quelle classe. Il est également beaucoup plus efficace que dynamic_cast <>.

15
Jerry Coffin

ici est un excellent article sur RTTI et pourquoi vous devrez peut-être en déployer votre propre version.

Je ne suis pas un expert du C++ RTTI, mais j'ai également implémenté mon propre RTTI car il y a certainement des raisons pour lesquelles vous auriez besoin de le faire. Tout d'abord, le système C++ RTTI n'est pas très riche en fonctionnalités, essentiellement tout ce que vous pouvez faire est de transtyper le type et d'obtenir des informations de base. Que faire si, au moment de l'exécution, vous avez une chaîne avec le nom d'une classe et que vous souhaitez construire un objet de cette classe, bonne chance avec C++ RTTI. De plus, C++ RTTI n'est pas vraiment (ou facilement) portable entre les modules (vous ne pouvez pas identifier la classe d'un objet qui a été créé à partir d'un autre module (dll/so ou exe). De même, l'implémentation de C++ RTTI est spécifique au compilateur, et il est généralement coûteux d'activer en termes de frais supplémentaires pour l'implémenter pour tous les types. Enfin, il n'est pas vraiment persistant, donc il ne peut pas vraiment être utilisé pour l'enregistrement/le chargement de fichiers par exemple (par exemple, vous voudrez peut-être enregistrer le les données d'un objet dans un fichier, mais vous voudrez également enregistrer le "typeid" de sa classe, de telle sorte qu'au moment du chargement, vous savez quel objet créer pour charger ces données, cela ne peut pas être fait de manière fiable avec C++ RTTI). Pour tout ou partie de ces raisons, de nombreux frameworks ont leur propre RTTI (du très simple au très riche en fonctionnalités). Les exemples sont wxWidget, LLVM, Boost.Serialization, etc. ce n'est vraiment pas si rare.

Puisque vous avez juste besoin d'une méthode virtuelle pour avoir une table virtuelle, pourquoi ne marquent-ils pas simplement une méthode comme virtuelle? Les destructeurs virtuels semblent être bons dans ce domaine.

C'est probablement ce que leur système RTTI utilise également. Les fonctions virtuelles sont la base de la liaison dynamique (liaison au moment de l'exécution) et, par conséquent, elles sont fondamentalement requises pour effectuer tout type d'identification/information de type au moment de l'exécution (non seulement requis par le C++ RTTI, mais toute implémentation de RTTI aura s'appuyer sur les appels virtuels d'une manière ou d'une autre).

Si leur solution n'utilise pas de RTTI ordinaire, une idée de la façon dont elle a été mise en œuvre?

Bien sûr, vous pouvez rechercher des implémentations RTTI en C++. J'ai fait le mien et de nombreuses bibliothèques ont également leur propre RTTI. C'est assez simple à écrire, vraiment. Fondamentalement, tout ce dont vous avez besoin est un moyen de représenter de manière unique un type (c'est-à-dire le nom de la classe, ou une version modifiée de celui-ci, ou même un ID unique pour chaque classe), une sorte de structure analogue à type_info qui contient toutes les informations sur le type dont vous avez besoin, alors vous avez besoin d'une fonction virtuelle "cachée" dans chaque classe qui retournera ce type d'informations sur demande (si cette fonction est remplacée dans chaque classe dérivée, cela fonctionnera). Il y a, bien sûr, des choses supplémentaires qui peuvent être faites, comme un référentiel singleton de tous types, peut-être avec des fonctions d'usine associées (cela peut être utile pour créer des objets d'un type lorsque tout ce qui est connu au moment de l'exécution est le nom du type, sous forme de chaîne ou d'ID de type). En outre, vous souhaiterez peut-être ajouter des fonctions virtuelles pour autoriser la conversion de type dynamique (cela se fait généralement en appelant la fonction de conversion de la classe la plus dérivée et en exécutant static_cast jusqu'au type vers lequel vous souhaitez caster).

10
Mikael Persson

La raison prédominante est qu'ils ont du mal à maintenir l'utilisation de la mémoire aussi faible que possible.

RTTI n'est disponible que pour les classes qui comportent au moins une méthode virtuelle, ce qui signifie que les instances de la classe contiendront un pointeur vers la table virtuelle.

Sur une architecture 64 bits (ce qui est courant aujourd'hui), un seul pointeur fait 8 octets. Comme le compilateur instancie de nombreux petits objets, cela s'additionne assez rapidement.

Par conséquent, il y a un effort continu pour supprimer les fonctions virtuelles autant que possible (et pratique) et implémenter ce qui aurait été des fonctions virtuelles avec l'instruction switch, qui a une vitesse d'exécution similaire mais un impact mémoire considérablement plus faible.

Leur souci constant de la consommation de mémoire a porté ses fruits, dans la mesure où Clang consomme beaucoup moins de mémoire que gcc, par exemple, ce qui est important lorsque vous offrez la bibliothèque aux clients.

D'un autre côté, cela signifie également que l'ajout d'un nouveau type de nœud entraîne généralement la modification du code dans un bon nombre de fichiers car chaque commutateur doit être adapté (heureusement, les compilateurs émettent un avertissement si vous manquez un membre enum dans un commutateur). Ils ont donc accepté de rendre la maintenance un peu plus difficile au nom de l'efficacité de la mémoire.

4
Matthieu M.