Avant de lire la question:
Cette question n'est pas de savoir à quel point il est utile d'utiliser dynamic_cast
. C'est à peu près ses performances.
J'ai récemment développé un design où dynamic_cast
est beaucoup utilisé.
En discutant avec des collègues, presque tout le monde dit que dynamic_cast
ne doit pas être utilisé en raison de ses mauvaises performances (ce sont des collègues qui ont des antécédents différents et dans certains cas ne se connaissent pas. Je travaille dans une grande entreprise)
J'ai décidé de tester les performances de cette méthode au lieu de les croire.
Le code suivant a été utilisé:
ptime firstValue( microsec_clock::local_time() );
ChildObject* castedObject = dynamic_cast<ChildObject*>(parentObject);
ptime secondValue( microsec_clock::local_time() );
time_duration diff = secondValue - firstValue;
std::cout << "Cast1 lasts:\t" << diff.fractional_seconds() << " microsec" << std::endl;
Le code ci-dessus utilise des méthodes de boost::date_time
sous Linux pour obtenir des valeurs utilisables.
J'ai fait 3 dynamic_cast
en une seule exécution, le code pour les mesurer est le même.
Les résultats d'une exécution ont été les suivants:
Cast1 dure: 74 microsecondes
Cast2 dure: 2 microsecondes
Cast3 dure: 1 microsec
La première distribution a toujours pris 74-111 microsecondes, les moulages suivants dans la même exécution ont pris 1-3 microsecondes.
Alors enfin mes questions:
Est dynamic_cast
vraiment mauvais?
Selon les résultats du test, ce n'est pas le cas. Mon code de test est-il correct?
Pourquoi tant de développeurs pensent-ils que c'est lent sinon?
Tout d'abord, vous devez mesurer les performances sur bien plus que quelques itérations, car vos résultats seront dominés par la résolution de la minuterie. Essayez par exemple 1 million +, afin de construire une image représentative. De plus, ce résultat n'a de sens que si vous le comparez à quelque chose, c'est-à-dire en faisant l'équivalent mais sans le casting dynamique.
Deuxièmement, vous devez vous assurer que le compilateur ne vous donne pas de faux résultats en optimisant plusieurs conversions dynamiques sur le même pointeur (utilisez donc une boucle, mais utilisez à chaque fois un pointeur d'entrée différent).
La conversion dynamique sera plus lente, car elle doit accéder à la table RTTI (informations de type au moment de l'exécution) pour l'objet et vérifier que la conversion est valide. Ensuite, pour l'utiliser correctement, vous devrez ajouter un code de gestion des erreurs qui vérifie si le pointeur renvoyé est NULL
. Tout cela prend des cycles.
Je sais que vous ne vouliez pas en parler, mais "un design où dynamic_cast est beaucoup utilisé" est probablement un indicateur que vous faites quelque chose de mal ...
Les performances n'ont aucun sens sans comparer des fonctionnalités équivalentes. La plupart des gens disent que dynamic_cast est lent sans se comparer à un comportement équivalent. Appelez-les à ce sujet. En d'autres termes:
Si "fonctionne" n'est pas une exigence, je peux écrire du code qui échoue plus rapidement que le vôtre.
Il existe différentes manières d'implémenter dynamic_cast, et certaines sont plus rapides que d'autres. Stroustrup a publié un article sur l'utilisation de nombres premiers pour améliorer dynamic_cast , par exemple. Malheureusement, il est inhabituel de contrôler la façon dont votre compilateur implémente le transtypage, mais si les performances comptent vraiment pour vous, alors vous avez le contrôle sur le compilateur que vous utilisez.
Cependant, ne pas utiliser dynamic_cast sera toujours plus rapide que de l'utiliser - mais si vous n'avez pas réellement besoin de dynamic_cast, alors ne l'utilisez pas! Si vous avez besoin d'une recherche dynamique, il y aura des frais généraux et vous pourrez ensuite comparer différentes stratégies.
Voici quelques repères:
http://tinodidriksen.com/2010/04/14/cpp-dynamic-cast-performance/
http://www.nerdblog.com/2006/12/how-slow-is-dynamiccast.html
Selon eux, dynamic_cast est 5 à 30 fois plus lent que reinterpret_cast, et la meilleure alternative fonctionne presque de la même manière que reinterpret_cast.
Je vais citer la conclusion du premier article:
- dynamic_cast est lent pour autre chose que la conversion vers le type de base; cette distribution particulière est optimisée
- le niveau d'héritage a un grand impact sur dynamic_cast
- variable membre + reinterpret_cast est le moyen fiable le plus rapide pour
déterminer le type; cependant, cela a des frais généraux de maintenance beaucoup plus élevés
lors du codage
Les nombres absolus sont de l'ordre de 100 ns pour une seule distribution. Des valeurs comme 74 msec ne semblent pas proches de la réalité.
Désolé de le dire, mais votre test est pratiquement inutile pour déterminer si le casting est lent ou non. La résolution en microsecondes est loin d'être suffisante. Nous parlons d'une opération qui, même dans le pire des cas, ne devrait pas prendre plus de, disons, 100 tics d'horloge, ou moins de 50 nanosecondes sur un PC typique.
Il ne fait aucun doute que la distribution dynamique sera plus lente qu'une distribution statique ou une distribution réinterprétée, car, au niveau de l'assemblage, les deux derniers constitueront une affectation (vraiment rapide, ordre de 1 tick d'horloge), et la distribution dynamique nécessite le code pour aller inspecter l'objet pour déterminer son type réel.
Je ne peux pas dire d'emblée à quel point c'est lent, cela varierait probablement d'un compilateur à l'autre, j'aurais besoin de voir le code d'assembly généré pour cette ligne de code. Mais, comme je l'ai dit, 50 nanosecondes par appel est la limite supérieure de ce qui devrait être raisonnable.
Votre kilométrage peut varier, pour minimiser la situation.
Les performances de dynamic_cast dépendent en grande partie de ce que vous faites et peuvent dépendre du nom des classes (et la comparaison du temps par rapport à reinterpet_cast
Semble étrange, car dans la plupart des cas, aucune instruction pratique ne prend zéro fins, comme par exemple une conversion de unsigned
vers int
).
J'ai cherché à savoir comment cela fonctionne dans clang/g ++. En supposant que vous dynamic_cast
Passez d'un B*
À un D*
, Où B
est une base (directe ou indirecte) de D
, et sans tenir compte des complications de plusieurs classes de base, il semble fonctionner en appelant une fonction de bibliothèque qui fait quelque chose comme ceci:
for dynamic_cast<D*>( p ) where p is B*
type_info const * curr_typ = &typeid( *p );
while(1) {
if( *curr_typ == typeid(D)) { return static_cast<D*>(p); } // success;
if( *curr_typ == typeid(B)) return nullptr; //failed
curr_typ = get_direct_base_type_of(*curr_typ); // magic internal operation
}
Donc, oui, c'est assez rapide quand *p
Est en fait un D
; une seule comparaison type_info
réussie. Le pire des cas est lorsque la conversion échoue, et il y a beaucoup d'étapes de D
à B
; dans ce cas, il y a beaucoup de comparaisons de types qui ont échoué.
Combien de temps dure la comparaison de types? il le fait, sur clang/g ++:
compare_eq( type_info const &a, type_info const & b ){
if( &a == &b) return true; // same object
return strcmp( a.name(), b.name())==0;
}
Le strcmp est nécessaire car il est possible d'avoir deux objets type_info
Différents représentant le même type (bien que je suis à peu près sûr que cela ne se produit que lorsque l'un est dans une bibliothèque partagée et l'autre pas dans cette bibliothèque). Mais, dans la plupart des cas, lorsque les types sont réellement égaux, ils font référence au même type_info; ainsi la plupart des comparaisons de type réussi sont très rapides.
La méthode name()
renvoie simplement un pointeur sur une chaîne fixe contenant le nom modifié de la classe. Il y a donc un autre facteur: si de nombreuses classes sur le chemin de D
à B
ont des noms commençant par MyAppNameSpace::AbstractSyntaxNode<
, Alors les comparaisons échouées prendront plus de temps que d'habitude; le strcmp n'échouera pas jusqu'à ce qu'il atteigne une différence dans les noms de type déformés.
Et, bien sûr, puisque l'opération dans son ensemble traverse un tas de structures de données liées représentant la hiérarchie de types, le temps dépendra de si ces choses sont fraîches dans le cache ou non. Ainsi, le même casting répété est susceptible d'afficher un temps moyen qui ne représente pas nécessairement les performances typiques de ce casting.