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:
- Copiez certains registres de la pile pour permettre à la fonction appelée d’utiliser ces registres.
- Copiez les arguments dans des emplacements prédéfinis, de sorte que la fonction appelée puisse les trouver indépendamment de l'endroit où elle est appelée.
- Poussez l'adresse de retour.
- Branche / saute au code de la fonction, qui est une adresse de compilation et donc codée en dur dans le binaire par le compilateur / éditeur de liens.
- Obtenez la valeur de retour à partir d'un emplacement prédéfini et restaurez les registres que vous souhaitez utiliser.
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 ...
- Obtenez le pointeur vtable, qui pointe vers un tableau de pointeurs de fonction (adresses de fonction), un pour chaque fonction virtuelle, à partir de l'objet.
- Récupère la bonne adresse de la table vtable dans un registre (l’index où l’adresse de la bonne fonction est stockée est décidé au moment de la compilation).
- Accédez à l'adresse de ce registre plutôt que de passer à une adresse codée en dur.
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
, des switch
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.