Quelqu'un peut-il expliquer en détail comment fonctionne exactement la table virtuelle et quels pointeurs sont associés lorsque des fonctions virtuelles sont appelées.
S'ils sont en fait plus lents, pouvez-vous indiquer que le temps nécessaire à l'exécution d'une fonction virtuelle est plus long que les méthodes de classe normales? Il est facile de perdre la trace de comment / ce qui se passe sans voir un code.
Réponses:
Les méthodes virtuelles sont généralement implémentées via des tables de méthodes virtuelles (vtable en abrégé), dans lesquelles les pointeurs de fonction sont stockés. Cela ajoute un indirection à l’appel réel (il faut aller chercher l’adresse de la fonction à appeler depuis la table vtable, puis l’appeler - au lieu de simplement l’appeler tout de suite). Bien sûr, cela prend du temps et du code supplémentaire.
Cependant, ce n'est pas nécessairement la cause principale de la lenteur. Le vrai problème est que le compilateur (généralement / généralement) ne peut pas savoir quelle fonction sera appelée. Donc, il ne peut pas y insérer ou effectuer d’autres optimisations. Cela seul pourrait ajouter une douzaine d'instructions inutiles (préparer des registres, appeler puis rétablir l'état par la suite) et empêcher d'autres optimisations apparemment sans rapport. De plus, si vous branchez comme un fou en appelant de nombreuses implémentations différentes, vous subissez les mêmes hits que vous auriez du mal à vous faire ramifier comme un fou par d'autres moyens: le prédicteur de cache et de branche ne vous aidera pas, les branches prendront plus de temps que ce qui est parfaitement prévisible branche.
Gros mais : Ces succès sont généralement trop minimes pour être importants. Ils valent la peine d’être pris en compte si vous souhaitez créer un code hautes performances et envisagez d’ajouter une fonction virtuelle appelée à une fréquence alarmante. Cependant, gardez également à l’esprit que le remplacement des appels de fonctions virtuelles par d’autres moyens de créer des branches (
if .. else
,switch
, pointeurs de fonction, etc.) ne résoudra pas le problème fondamental - il peut très bien être plus lent. Le problème (s'il existe du tout) n'est pas les fonctions virtuelles mais l'indirection (inutile).Edit: La différence dans les instructions d’appel est décrite dans d’autres réponses. Fondamentalement, le code pour un appel statique ("normal") est le suivant:
Un appel virtuel fait exactement la même chose, à la différence que l’adresse de la fonction n’est pas connue au moment de la compilation. Au lieu de cela, quelques instructions ...
En ce qui concerne les branches: une branche est tout ce qui saute à une autre instruction au lieu de simplement exécuter l'instruction suivante. Cela inclut
if
, desswitch
parties de boucles diverses, des appels de fonction, etc. et parfois, le compilateur implémente des choses qui ne semblent pas créer de branche d’une manière qui nécessite réellement une branche sous le capot. Voir Pourquoi le traitement d'un tableau trié est-il plus rapide qu'un tableau non trié? car cela peut être lent, ce que les processeurs font pour contrer ce ralentissement et comment ce n’est pas une panacée.la source
virtual
.Voici un code désassemblé réel d'un appel de fonction virtuelle et d'un appel non virtuel, respectivement:
Vous pouvez constater que l'appel virtuel nécessite trois instructions supplémentaires pour rechercher l'adresse correcte, tandis que l'adresse de l'appel non virtuel peut être compilée.
Toutefois, notez que la plupart du temps, ce temps de recherche supplémentaire peut être considéré comme négligeable. Dans les situations où le temps de recherche serait important, comme dans une boucle, la valeur peut généralement être mise en cache en effectuant les trois premières instructions avant la boucle.
L'autre situation dans laquelle le temps de recherche devient important est si vous avez une collection d'objets et que vous passez en boucle en appelant une fonction virtuelle sur chacun d'eux. Cependant, dans ce cas, vous aurez besoin d' un moyen de sélectionner la fonction à appeler de toute façon, et une recherche dans une table virtuelle est un moyen aussi efficace que n'importe lequel. En fait, étant donné que le code de recherche vtable est si largement utilisé, il est fortement optimisé. Par conséquent, essayer de le contourner manuellement a de bonnes chances d’entraîner une dégradation des performances.
la source
-0x8(%rbp)
. oh mon ... cette syntaxe AT & T.Plus lent que quoi ?
Les fonctions virtuelles résolvent un problème qui ne peut pas être résolu par des appels de fonction directs. En général, vous ne pouvez comparer que deux programmes calculant la même chose. "Ce traceur de rayons est plus rapide que ce compilateur" n'a pas de sens, et ce principe se généralise même aux petites choses comme les fonctions individuelles ou les constructions de langage de programmation.
Si vous n'utilisez pas une fonction virtuelle pour basculer de manière dynamique sur un morceau de code basé sur une donnée, telle que le type d'un objet, vous devrez alors utiliser quelque chose d'autre,
switch
instruction, pour accomplir la même chose. Ce quelque chose d'autre a ses propres frais généraux, plus des implications sur l'organisation du programme qui influencent sa maintenabilité et sa performance globale.Notez qu'en C ++, les appels aux fonctions virtuelles ne sont pas toujours dynamiques. Lorsque des appels sont effectués sur un objet dont le type exact est connu (parce que l'objet n'est pas un pointeur ou une référence, ou parce que son type peut être inféré de manière statique), les appels ne sont que des appels de fonction membres normaux. Cela signifie non seulement qu'il n'y a pas de surcharge d'expédition, mais également que ces appels peuvent être intégrés de la même manière que les appels ordinaires.
En d'autres termes, votre compilateur C ++ peut fonctionner lorsque les fonctions virtuelles ne nécessitent pas de répartition virtuelle. Il n'y a donc généralement aucune raison de s'inquiéter de leurs performances par rapport aux fonctions non virtuelles.
Nouveau: De plus, nous ne devons pas oublier les bibliothèques partagées. Si vous utilisez une classe qui se trouve dans une bibliothèque partagée, l'appel d'une fonction membre ordinaire ne sera pas simplement une belle séquence d'instruction semblable à celle-ci
callq 0x4007aa
. Il doit passer par quelques étapes, comme indirecte via une "table de liaison de programme" ou une telle structure. Par conséquent, l'indirection indirecte des bibliothèques partagées pourrait niveler (sinon complètement) la différence de coût entre un appel virtuel (réellement indirect) et un appel direct. Le raisonnement sur les compromis de la fonction virtuelle doit donc tenir compte de la construction du programme: si la classe de l'objet cible est liée de manière monolithique au programme qui effectue l'appel.la source
parce qu'un appel virtuel équivaut à
où, avec une fonction non virtuelle, le compilateur peut plier de manière constante la première ligne, il s'agit d'un déréférencement d'un ajout et d'un appel dynamique transformé en un simple appel statique
cela lui permet également d’intégrer la fonction (avec toutes les conséquences de son optimisation)
la source