Quel est le coût d'avoir une méthode virtuelle dans une classe C++?
Ayant au moins une méthode virtuelle dans une classe C++ (ou l'une de ses classes parentes) signifie que la classe aura une table virtuelle, et chaque instance aura un pointeur virtuel.
De sorte que le coût mémoire est tout à fait clair. Le plus important est le coût mémoire sur les instances (en particulier si les instances sont de petite taille, par exemple, s'ils sont destinés à contenir un entier: dans ce cas avoir un pointeur virtuel dans tous les cas, peut doubler la taille des instances. Comme pour l'espace mémoire utilisé par les tables virtuelles, je suppose que c'est généralement négligeable par rapport à l'espace utilisé par le code de la méthode.
Cela m'amène à ma question: est-il mesurable de la performance des coûts (c'est à dire l'impact de la vitesse) pour la présentation d'une méthode virtuelle? Il y aura une recherche dans la table virtuelle au moment de l'exécution, lors de chaque appel de méthode, donc si il y a très souvent des appels à cette méthode, et si cette méthode est très court, alors il pourrait y avoir un rendement mesurables frapper? Je suppose que ça dépend de la plate-forme, mais quiconque a exécuté quelques repères?
La raison que je demande, c'est que je suis tombé sur un bug qui est arrivé à être en raison d'un programmeur oublier de définir une méthode virtuelle. Ce n'est pas la première fois que je vois ce genre d'erreur. Et j'ai pensé: pourquoi ne nous ajouter le mot clé virtual lorsque nécessaire, au lieu de retrait le mot clé virtual lorsque nous sommes absolument convaincus qu'il est pas nécessaire? Si le coût est faible, je pense que je vais tout simplement vous recommandons ce qui suit dans mon équipe: il suffit de faire chaque méthode virtuelle par défaut, y compris le destructeur, dans chaque classe, et supprimer uniquement lorsque vous en avez besoin pour. Est-ce que sembler fou pour vous?
- Voir aussi le Dépassement de Pile question: l'IA des Applications en C++: Comment coûteuses sont des fonctions virtuelles? Quelles sont les optimisations possibles?
- La comparaison virtuelle et non les appels virtuels n'est pas menaingfull. Ils offrent des fonctionnalités différentes. Si vous voulez comparer la fonction virtuelle appels contre les C equivelent vous devez ajouter le coût du code qui implémente la fonctionnalité équivalent de la fonction virtuelle.
- Qui est soit une instruction switch ou un grand si l'instruction. Si vous étiez intelligent, vous pourriez re-mettre en œuvre à l'aide d'un pointeur de fonction table, mais les probabilités de se tromper sont beaucoup plus élevés.
- Voir stackoverflow.com/questions/156257/...
- La question est au sujet de la fonction des appels qui n'ont pas besoin d'être virtuel, de sorte que la comparaison est pertinente.
- Rançon: oui, exactement, thx. Je suis à peu près en disant: "que faire si nous avons défini TOUTES les fonctions virtuelles, même ceux qui n'ont pas vraiment besoin de l'être, et, plus tard, de supprimer le mot clé virtual quand il est sûr à 100% qu'il n'est pas nécessaire". En procédant ainsi, aurions-nous éviter un tas de bugs et de perdre uniquement négligeable perf?
- Si vous utilisez Visual C++, envisager l'utilisation de "substitution" qui est un non-standard: msdn.microsoft.com/en-us/library/z8ew2153.aspx. C'est très bon pour la détection de ces sortes de bugs au moment de la compilation.
- double possible de les Performances pour travailler avec des interfaces en C++?
- Tout en faisant virtuel par défaut jusqu'à ce que quelqu'un justifie pourquoi il peut/doit être non-virtuel est un abominable politique, oui.
- Connexes: dans ma réponse à stackoverflow.com/questions/46579750/..., j'ai montré un cas où le coût de boucler sur un vecteur de pointeurs de l'appel d'une méthode virtuelle est peut-être 24x plus lent que boucler sur un vecteur d'objets directement, où le compilateur peut auto-vectorisation de après l'in-lining, le non-appel de fonction virtuelle. Donc, si vous avez le choix entre le maintien de plusieurs contenants pour les différents types d'objets contre de garder un tableau de pointeurs sur des objets mélangés, de laisser le compilateur inline petites fonctions est très bon.
Vous devez vous connecter pour publier un commentaire.
Je couru quelques timings sur un 3ghz dans l'ordre de processeur PowerPC. Sur cette architecture, un appel de fonction virtuelle coûte 7 nanosecondes plus que directe (non virtuelle) d'appel de fonction.
Donc, pas vraiment la peine de s'inquiéter du coût, sauf si la fonction est quelque chose comme un trivial Get()/Set() accesseur, dans lequel rien d'autre que dans la ligne est le genre de gaspillage. Un 7ns frais généraux sur une fonction qui inlines à 0,5 ns est grave; un 7ns-dessus sur une fonction qui prend de 500 ms à exécuter est dénuée de sens.
Le coût énorme de fonctions virtuelles n'est pas vraiment à la recherche d'un pointeur de fonction dans la vtable (qui est habituellement juste un seul cycle), mais que le sous-saut ne peuvent généralement pas être de la branche prédit. Cela peut entraîner un grand pipeline de la bulle que le processeur ne peut pas récupérer toutes les instructions jusqu'à ce que le saut indirect (l'appel à l'aide du pointeur de fonction) a pris sa retraite et un nouveau pointeur d'instruction calculée. Ainsi, le coût d'un appel de fonction virtuelle est beaucoup plus grand qu'il ne paraît à partir de la recherche à l'assemblée... mais toujours seulement 7 nanosecondes.
Edit: Andrew, Pas Sûr, et d'autres soulèvent également le très bon point qu'un appel de fonction virtuelle peut provoquer une cache d'instructions manquer: si vous sautez à une adresse de code qui n'est pas dans le cache, alors tout le programme arrive à un cul de tourner pendant que les instructions sont lues depuis la mémoire principale. C'est toujours un important décrochage: le Xénon, à environ 650 cycles (par mes tests).
Cependant ce n'est pas un problème spécifique à des fonctions virtuelles, parce que même directement appel de fonction sera la cause d'une miss si vous sauter aux instructions qui ne sont pas dans le cache. Ce qui importe est de savoir si la fonction a été exécutée avant que récemment (en les rendant plus susceptibles d'être dans le cache), et si votre architecture peut prédire statique (et non virtuel) des branches et de l'extraction de ces instructions dans le cache à l'avance. Mon PPC n'est pas, mais peut-être que Intel est plus un matériel récent, ne.
Mes timings de contrôler l'influence de l'icache manque à l'exécution (délibérément, car j'étais en train d'examiner le PROCESSEUR pipeline dans l'isolement), la réduction des coûts.
Il est certainement mesurables de gestion lors de l'appel d'une fonction virtuelle - l'appel doit utiliser la vtable pour résoudre l'adresse de la fonction pour ce type d'objet. Des instructions supplémentaires sont le moindre de vos soucis. Non seulement vtables prévenir de nombreuses possibilités d'optimisations du compilateur (puisque le type est polymorphe le compilateur) ils peuvent aussi thrash votre I-Cache.
De savoir si ces sanctions sont significatifs ou non dépend de votre application, comment, souvent, ces chemins de code sont exécutés, et vos modes de transmission.
À mon avis cependant, de tout ce que virtuel par défaut est une solution unique à un problème, vous pouvez résoudre par d'autres moyens.
Vous pourriez peut-être regarder comment les cours sont conçus et documentés/écrit. Généralement l'en-tête d'une classe devrait faire très clairement les fonctions qui peuvent être remplacées par des classes dérivées et la façon dont ils sont appelés. Avoir des programmeurs d'écrire cette documentation est utile pour s'assurer qu'elles sont marquées comme virtuelle.
Je dirais aussi que la déclaration de chaque fonction virtuelle pourrait conduire à plus de bugs que de simplement oublier de marquer quelque chose comme virtuelle. Si toutes les fonctions sont virtuels, tout peut être remplacé par des classes de base - public, protégé, privé, tout devient un jeu équitable. Par accident ou de l'intention qui les sous-classes pourrait alors modifier le comportement des fonctions qui causent des problèmes lors de son utilisation dans la base de la mise en œuvre.
save
qui s'appuie sur une mise en œuvre spécifique d'une fonctionwrite
dans la classe de base, alors il me semble que ce soitsave
est mal codé, ouwrite
doit être privé.final
mot-clé pour les méthodes, et 2) les méthodes privées sont automatiquement finale.Il dépend. 🙂 (Aviez-vous prévu quelque chose d'autre?)
Une fois une classe devient une fonction virtuelle, il ne peut plus être une GOUSSE de type de données, (il ne peut pas avoir été l'un avant, soit, dans ce cas, il ne fera pas une différence) et qui fait de toute une série d'optimisations impossible.
std::copy() sur la plaine de la GOUSSE types peuvent recourir à une simple memcpy de routine, mais non de la GOUSSE de types doivent être traités avec plus de soin.
Construction devient beaucoup plus lent, car la vtable doit être initialisé. Dans le pire des cas, la différence de performance entre la nacelle et de la non-POD types de données peuvent être importantes.
Dans le pire des cas, vous risquez de voir 5x plus lent d'exécution (ce numéro est tiré à partir d'un projet de l'université que j'ai fait récemment ré-écrire quelques classes de la bibliothèque standard. Notre conteneur a pris environ 5x plus long à construire dès que le type de données stockées eu une vtable)
Bien sûr, dans la plupart des cas, vous êtes peu probable de voir les performances mesurables différence, c'est simplement pour souligner que dans le certains frontière des cas, il peut être très coûteux.
Cependant, les performances ne devraient pas être votre principale considération ici.
Rendre tout ce qui est virtuel n'est pas une solution parfaite pour d'autres raisons.
De permettre à tout être redéfinie dans les classes dérivées rend beaucoup plus difficile le maintien de la classe d'invariants. Comment une classe de garantir qu'il reste dans un état cohérent lorsque l'une quelconque de ses méthodes pourrait être redéfini à tout moment?
Rendre tout ce qui est virtuel peut éliminer quelques bugs potentiels, mais il introduit aussi de nouveaux.
Si vous avez besoin de la fonctionnalité de virtuel d'expédition, vous avez à payer le prix. L'avantage du C++, c'est que vous pouvez utiliser un moyen très efficace de mise en œuvre de virtuel d'expédition fourni par le compilateur, plutôt que d'une éventuelle inefficacité de la version que vous mettre en œuvre vous-même.
Cependant, l'exploitation forestière-vous avec la surcharge si vous n'avez pas needx il est peut-être aller un peu trop loin. Et la plupart des classesare pas conçu pour être hérité de - pour créer une bonne classe de base exige plus que faire de ses fonctions virtuelles.
Virtuel de l'expédition est d'un ordre de grandeur inférieure à celle des alternatives - non pas par indirection tellement que la prévention de l'in-lining. Ci-dessous, je montre que, par contraste virtuel expédition avec une mise en œuvre à l'incorporation d'un "type(-numéro d'identification)", dans les objets et à l'aide d'une instruction switch pour sélectionner le type de code spécifiques. Cela évite d'appel de fonction, les frais généraux complètement - il suffit de faire un local de saut. Il y a un coût potentiel de la maintenabilité, de la recompilation des dépendances etc par le biais de la contrainte de localisation (l'interrupteur) du type de fonctionnalité spécifique.
Mise en ŒUVRE
RÉSULTATS DE PERFORMANCE
Sur mon système Linux:
Ceci suggère une ligne de type-nombre de commutation approche est fondée sur des (1.28 - 0.23) /(0.344 - 0.23) = 9.2 fois plus rapide. Bien sûr, qui est spécifique à l'exacte du système testé /drapeaux du compilateur & version etc., mais généralement à titre indicatif.
OBSERVATIONS VIRTUELLES ENVOI
Il faut quand-même dire que l'appel de fonction virtuelle frais généraux sont quelque chose qui est rarement significative, et alors seulement pour souvent appelé trivial fonctions (comme les getters et setters). Même alors, vous pourriez être en mesure de fournir une seule fonction pour obtenir et de définir un ensemble de choses à la fois, tout en minimisant les coûts. Les gens s'inquiètent au sujet de virtual chemin d'expédition trop - afin de faire le profilage avant de trouver maladroit alternatives. Le principal problème avec ces derniers est qu'ils sont hors de la ligne d'appel de la fonction, s'ils ont également délocaliser le code exécuté qui change de l'utilisation du cache des modèles (pour le meilleur ou (plus souvent) pour le pire).
g++
/clang
et-lrt
. J'ai pensé qu'il était utile de mentionner ici, pour les futurs lecteurs.Le coût supplémentaire est que pratiquement rien dans la plupart des scénarios. (pardonnez le calembour). ejac a déjà posté sensible mesures relatives.
La chose la plus importante que vous donnez est optimisations possibles en raison de l'in-lining. Ils peuvent être particulièrement bonne si la fonction est appelée avec des paramètres uniques. C'est rarement fait une réelle différence, mais dans quelques cas, cela peut être énorme.
Concernant les optimisations:
Il est important de connaître et de considérer le coût relatif de constructions de votre langue. Big O la notation est onl la moitié de l'histoire - comment votre application à l'échelle. L'autre moitié est le facteur constant en face d'elle.
En règle générale, je n'irais pas hors de mon chemin afin d'éviter des fonctions virtuelles, à moins qu'il y a de clair et précis des indications qu'il est un goulot de bouteille. Un design épuré vient toujours en premier - mais c'est une seule des parties prenantes qui ne devrait pas indûment du mal aux autres.
Exemple artificiel: Un destructeur virtuel vide sur un tableau d'un million de petits éléments qui peuvent labourer par le biais d'au moins 4 mo de données, la raclée de votre cache. Si destructeur peut être incorporé à l'écart, les données ne seront pas touchés.
Lors de l'écriture de code de bibliothèque, de telles considérations sont loin d'être prématuré. Vous ne savez jamais combien de boucles seront mis autour de votre fonction.
Alors que tout le monde est correct sur les performances des méthodes virtuelles et de ce fait, je pense que le vrai problème est de savoir si l'équipe connaît la définition du mot-clé virtuel en C++.
Considérer ce code, quelle est la sortie?
Rien de surprenant ici:
Que rien n'est virtuel. Si le mot-clé virtuel est ajouté à l'avant de Foo dans les deux classes A et B, nous obtenons ceci pour la sortie:
À peu près ce que tout le monde attend.
Maintenant, vous avez mentionné qu'il y a des bugs parce que quelqu'un a oublié d'ajouter un mot clé virtual. Considérez donc ce code (où le mot-clé virtuel est ajouté à Un, mais pas de classe B). Qu'est-ce que la sortie alors?
Réponse: Le même que si le mot-clé virtuel est ajouté à un point B? La raison en est que la signature de B::Foo correspond exactement que A::Foo() et parce que l'Un des Foo est virtuel, donc, est B.
Considérons maintenant le cas où B Foo est virtuelle et ne l'est pas. Qu'est-ce que la sortie alors? Dans ce cas, la sortie est
Le mot clé virtual fonctionne vers le bas dans la hiérarchie, non pas vers le haut. Il ne fait jamais les méthodes de classe de base virtuelle. La première fois, une méthode virtuelle est rencontré dans la hiérarchie, c'est quand le polymorphisme commence. Il n'y a pas un moyen pour plus tard classes pour les classes précédentes ont des méthodes virtuelles.
N'oubliez pas que les méthodes virtuelles dire que cette classe est de donner aux futures classes la possibilité de modifier/modifier certains de ses comportements.
Donc, si vous avez une règle à supprimer le mot-clé virtuel, il peut ne pas avoir l'effet escompté.
Le mot clé virtual dans C++ est un concept puissant. Assurez-vous que chaque membre de l'équipe sait vraiment ce concept, de sorte qu'il peut être utilisé comme prévu.
En fonction de votre plate-forme, la surcharge d'un appel virtuel peut être très indésirable. En déclarant chaque fonction virtuelle, vous êtes essentiellement en les appelant par l'intermédiaire d'un pointeur de fonction. À tout le moins, ce est un supplément de déréférencement, mais sur certaines plateformes PPC, il va utiliser le microcoded ou autrement lenteur des instructions pour ce faire.
Je le recommande à l'encontre de votre suggestion pour cette raison, mais si elle vous aide à empêcher les insectes, alors il peut être une valeur de l'échange. Je ne peux pas aider mais pense qu'il doit y avoir un moyen terme, qui vaut la peine de trouver, si.
Il faudra juste un couple d'extra asm instruction de l'appel de méthode virtuelle.
Mais je ne pense pas que vous vous inquiétez à ce que le plaisir(int a, int b) est un couple d'extra "pousser" des instructions par rapport à l'amusement(). Alors ne vous inquiétez pas virtuals trop, jusqu'à ce que vous êtes dans une situation particulière et voir qu'elle est vraiment conduit à des problèmes.
P. S. Si vous avez une méthode virtuelle, assurez-vous d'avoir un destructeur virtuel. De cette façon, vous éviterez d'éventuels problèmes
En réponse à la "xtofl" et " Tom " des commentaires. J'ai fait des petits tests avec 3 fonctions:
Mon test était une simple itération:
Et voici les résultats:
Il a été compilé par VC++ en mode de débogage. Je n'ai fait que 5 tests par la méthode et calculé la valeur moyenne (donc les résultats peuvent être assez imprécise)... de Toute manière, les valeurs sont presque égaux en supposant que 100 millions d'appels. Et la méthode avec un supplément de 3 push/pop a été plus lent.
Le point principal est que si vous n'aimez pas l'analogie avec le push/pop, pensez supplémentaire si/d'autre dans votre code? Pensez-vous à propos du PROCESSEUR pipeline lorsque vous ajoutez un supplément si/d'autre 😉 Aussi, on ne sait jamais sur quel PROCESSEUR le code sera exécuté... d'Habitude compilateur génère un code plus optimal pour un CPU et moins optimale pour un autre (Le Compilateur Intel C++ )
final
dans votre remplacement et vous avez un pointeur vers le type dérivé, plutôt que le type de base). Ce test, appelé de la même fonction virtuelle à chaque fois, donc il a prédit parfaitement; l'absence de canalisation bulles des autres, sauf à partir de peu d'call
débit. Et que lescall
peut-être un couple de plus d'uop. Direction de la prévision fonctionne bien, même indirecte, des branches, surtout si elles sont toujours à la même destination.call
que pour un directcall
. (Et oui, normalcall
instructions nécessité de prédiction de trop. Le parcours de l'étape a savoir l'adresse suivante pour récupérer avant ce bloc est décodé, il a donc à prévoir le prochain extraction de bloc basé sur le courant adresse de bloc, plutôt que de l'adresse d'instruction. Ainsi que de prévoir où, dans ce bloc il y a une branche de l'instruction...)