Question Existe-t-il une pénalité / un coût de l'héritage virtuel en C ++, lors de l'appel d'une méthode de base non virtuelle?


L'utilisation de l'héritage virtuel en C ++ entraîne-t-elle une pénalité d'exécution dans le code compilé, lorsque nous appelons un fonction régulière membre de sa classe de base? Exemple de code:

class A {
    public:
        void foo(void) {}
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

// ...

D bar;
bar.foo ();

20
2018-04-05 14:50


origine


Réponses:


Il peut y avoir, oui, si vous appelez la fonction membre via un pointeur ou une référence et que le compilateur ne peut pas déterminer avec une certitude absolue le type d'objet auquel le pointeur ou les points de référence font référence. Par exemple, considérez:

void f(B* p) { p->foo(); }

void g()
{
    D bar;
    f(&bar);
}

En supposant l'appel à f n'est pas en ligne, le compilateur doit générer du code pour trouver l'emplacement du A sous-objet de classe de base virtuelle afin d'appeler foo. Généralement, cette recherche implique la vérification de vptr / vtable.

Si le compilateur connaît le type d'objet sur lequel vous appelez la fonction, (comme c'est le cas dans votre exemple), il ne doit pas y avoir de surcharge car l'appel de fonction peut être distribué de manière statique (au moment de la compilation). Dans votre exemple, le type dynamique de bar est connu pour être D (il ne peut rien y avoir d'autre), donc l'offset du sous-objet de la classe de base virtuelle A peut être calculé au moment de la compilation.


15
2018-04-05 14:56



Oui, l'héritage virtuel a une surcharge d'exécution. C'est parce que le compilateur, pour tout pointeur / référence à objet, ne peut pas trouver ses sous-objets au moment de la compilation. En revanche, pour l'héritage unique, chaque sous-objet est situé à un décalage statique de l'objet d'origine. Considérer:

class A { ... };
class B : public A { ... }

La disposition en mémoire de B ressemble un peu à ceci:

| B's stuff | A's stuff |

Dans ce cas, le compilateur sait où est A. Cependant, considérez maintenant le cas de MVI.

class A { ... };
class B : public virtual A { ... };
class C : public virtual A { ... };
class D : public C, public B { ... };

La disposition de la mémoire de B:

| B's stuff | A's stuff |

La disposition de la mémoire de C:

| C's stuff | A's stuff |

Mais attendez! Lorsque D est instancié, cela ne ressemble pas à ça.

| D's stuff | B's stuff | C's stuff | A's stuff |

Maintenant, si vous avez un B *, s'il pointe vraiment sur un B, alors A est juste à côté du B- mais s'il pointe vers un D, ​​alors pour obtenir A *, vous devez vraiment passer le sous-marin C -object, et depuis n'importe quel donné B* pourrait pointer dynamiquement sur un B ou un D à l'exécution, vous devrez alors modifier dynamiquement le pointeur. Cela signifie, au minimum, que vous devrez produire du code pour trouver cette valeur par quelque moyen, par opposition à la valeur intégrée à la compilation, ce qui se produit pour l'héritage simple.


10
2018-04-05 15:42



Au moins dans une implémentation classique, l'héritage virtuel entraîne une (petite!) Pénalité pour (au moins certains) accès aux membres de données. En particulier, vous obtenez normalement un niveau supplémentaire d'indirection pour accéder aux données membres de l'objet dont vous avez dérivée virtuellement. Cela vient du fait que (au moins dans le cas normal) deux classes dérivées distinctes ou plus ont non seulement la même classe de base, mais la même classe de base. objet. Pour ce faire, les deux classes dérivées ont des pointeurs sur le même décalage dans l'objet le plus dérivé et accèdent à ces membres de données via ce pointeur.

Bien que ce soit techniquement pas à cause de l'héritage virtuel, il est probablement intéressant de noter qu'il existe une pénalité distincte (encore une fois) pour l'héritage multiple en général. Dans une implémentation typique de unique l'héritage, vous avez un pointeur vtable à un décalage fixe dans l'objet (assez souvent au tout début). Dans le cas d'un héritage multiple, vous ne pouvez évidemment pas avoir deux pointeurs vtable au même décalage. Vous obtenez donc un certain nombre de pointeurs vtable, chacun avec un décalage distinct dans l'objet.

IOW, le pointeur vtable avec un seul héritage est normalement juste static_cast<vtable_ptr_t>(object_address), mais avec l'héritage multiple, vous obtenez static_cast<vtable_ptr_t>(object_address+offset).

Techniquement, les deux sont entièrement séparés - mais bien sûr, la seule utilisation de l'héritage virtuel est associée à l'héritage multiple.


6
2018-04-05 15:49



Concrètement, dans Microsoft Visual C ++, il existe une différence réelle dans les tailles de pointeur à membre. Voir #pragma pointers_to_members. Comme vous pouvez le voir dans cette liste, la méthode la plus générale est "l'héritage virtuel", qui se distingue de l'héritage multiple, lequel est distinct de l'héritage unique.

Cela implique que davantage d'informations sont nécessaires pour résoudre un pointeur vers un membre en cas de présence d'héritage virtuel et que cela aura un impact sur les performances, ne serait-ce que par la quantité de données absorbée dans le cache du processeur. la longueur de la recherche du membre ou le nombre de sauts nécessaires.


2
2018-04-05 15:00



Je pense qu'il n'y a pas de pénalité d'exécution pour l'héritage virtuel. Ne confondez pas l'héritage virtuel avec les fonctions virtuelles. Les deux sont deux choses différentes. 

l'héritage virtuel garantit que vous n'avez qu'un seul sous-objet A dans les cas de D. Donc, je ne pense pas qu'il y aurait une pénalité à l'exécution pour cela seul.

Cependant, il peut survenir des cas où ce sous-objet ne peut pas être connu au moment de la compilation, de sorte que dans de tels cas, il y aurait une pénalité d'exécution pour l'héritage virtuel. Un tel cas est décrit par James dans sa réponse.


2
2018-04-05 14:57



Votre question est principalement axée sur l'appel ordinaire fonctions de la base virtuelle, pas le cas (loin) plus intéressant de virtuel fonctions de la classe de base virtuelle (classe A dans votre exemple) - mais oui, il peut y avoir un coût. Bien sûr, tout dépend du compilateur.

Lorsque le compilateur compilait A :: foo, il supposait que "this" pointe au début de l'endroit où les membres de données pour A résident en mémoire. À ce stade, le compilateur peut ne pas savoir que la classe A sera une base virtuelle de toute autre classe. Mais cela génère le code avec plaisir.

Maintenant, quand le compilateur compile B, il n'y aura pas vraiment de changement car alors que A est une classe de base virtuelle, il s'agit toujours d'un héritage et dans le cas typique, le compilateur mettra la classe B en place par les membres de données de la classe B, un B * peut être immédiatement converti en A * sans aucune modification de valeur et, par conséquent, aucun ajustement n'est nécessaire. Le compilateur peut appeler A :: foo en utilisant le même pointeur "this" (même s'il est de type B *) et il n'y a pas de danger.

La même situation est pour la classe C - son héritage toujours unique, et le compilateur typique placera les membres de données de A immédiatement suivis par les membres de données de C afin qu'un C * puisse être immédiatement converti en A * sans changement de valeur. Ainsi, le compilateur peut simplement appeler A :: foo avec le même pointeur "this" (même s'il est de type C *) et qu'il n'y a pas de mal.

Cependant, la situation est totalement différente pour la classe D. La disposition de la classe D sera généralement celle des membres de la classe A, suivis par les membres de la classe B, suivis par les membres de la classe C, suivis des membres de la classe.

En utilisant la mise en page typique, un D * peut être converti immédiatement en A *, donc il n'y a pas de pénalité pour A :: foo-- le compilateur peut appeler la même routine générée pour A :: foo sans modifier "this" et tout va bien.

Cependant, la situation change si le compilateur doit appeler une fonction membre telle que C :: other_member_func, même si C :: other_member_func est non-virtuel. La raison en est que lorsque le compilateur a écrit le code pour C :: other_member_func, il a supposé que la disposition de données référencée par le pointeur "this" correspond aux membres de données de A immédiatement suivis par les membres de données de C. Mais ce n'est pas vrai pour une instance de D. Le compilateur devra peut-être réécrire et créer un D :: other_member_func (non virtuel), juste pour prendre en compte la différence de disposition de la mémoire de l'instance de classe.

Notez que ceci est une situation différente mais similaire lors de l'utilisation de l'héritage multiple, mais dans plusieurs héritages sans bases virtuelles, le compilateur peut tout prendre en charge en ajoutant simplement un déplacement ou une correction au pointeur "this" "incorporé" dans une instance d'une classe dérivée. Mais avec les bases virtuelles, il est parfois nécessaire de réécrire une fonction. Tout dépend de quelles données sont accessibles les membres par la fonction membre (même non virtuelle) appelée.

Par exemple, si la classe C définissait une fonction membre non virtuelle C :: some_member_func, le compilateur devra peut-être écrire:

  1. C :: some_member_func lorsqu'il est appelé à partir d'une instance réelle de C (et non de D), comme déterminé au moment de la compilation (car some_member_func n'est pas une fonction virtuelle)
  2. C :: some_member_func lorsque la même fonction membre est appelée à partir d'une instance réelle de la classe D, déterminée au moment de la compilation. (Techniquement, cette routine est D :: some_member_func. Même si la définition de cette fonction membre est implicite et identique au code source de C :: some_member_func, le code objet généré peut être légèrement différent.)

si le code de C :: some_member_func arrive à utiliser des variables membres définies à la fois dans la classe A et la classe C.


1
2018-02-16 09:05