简体   繁体   English

调用非虚拟基方法时,C++ 中的虚拟 inheritance 是否有任何惩罚/成本?

[英]Is there any penalty/cost of virtual inheritance in C++, when calling non-virtual base method?

Does using virtual inheritance in C++ have a runtime penalty in compiled code, when we call a regular function member from its base class?当我们从其基数 class 调用常规 function成员时,在 C++ 中使用虚拟 inheritance 是否会在编译代码中产生运行时惩罚? Sample 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 ();

There may be, yes, if you call the member function via a pointer or reference and the compiler can't determine with absolute certainty what type of object that pointer or reference points or refers to.可能有,是的,如果您通过指针或引用调用成员函数,并且编译器无法绝对确定该指针或引用指向或引用的对象类型。 For example, consider:例如,考虑:

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

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

Assuming the call to f is not inlined, the compiler needs to generate code to find the location of the A virtual base class subobject in order to call foo .假设对f的调用不是内联的,编译器需要生成代码来查找A虚拟基类子对象的位置,以便调用foo Usually this lookup involves checking the vptr/vtable.通常这个查找涉及检查 vptr/vtable。

If the compiler knows the type of the object on which you are calling the function, though (as is the case in your example), there should be no overhead because the function call can be dispatched statically (at compile time).如果编译器知道您调用函数的对象的类型(如您的示例中的情况),则应该没有开销,因为可以静态调度函数调用(在编译时)。 In your example, the dynamic type of bar is known to be D (it can't be anything else), so the offset of the virtual base class subobject A can be computed at compile time.在您的示例中, bar的动态类型已知为D (它不能是其他任何东西),因此可以在编译时计算虚拟基类子对象A的偏移量。

Yes, virtual inheritance has a run-time performance overhead.是的,虚拟继承有运行时性能开销。 This is because the compiler, for any pointer/reference to object, cannot find it's sub-objects at compile-time.这是因为对于任何指向对象的指针/引用,编译器在编译时都找不到它的子对象。 In constrast, for single inheritance, each sub-object is located at a static offset of the original object.相比之下,对于单继承,每个子对象都位于原始对象的静态偏移处。 Consider:考虑:

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

The memory layout of B looks a little like this: B 的内存布局看起来有点像这样:

| B's stuff | A's stuff |

In this case, the compiler knows where A is.在这种情况下,编译器知道 A 在哪里。 However, now consider the case of MVI.但是,现在考虑 MVI 的情况。

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

B's memory layout: B的内存布局:

| B's stuff | A's stuff |

C's memory layout: C的内存布局:

| C's stuff | A's stuff |

But wait!可是等等! When D is instantiated, it doesn't look like that.当 D 被实例化时,它看起来不是那样的。

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

Now, if you have a B*, if it really points to a B, then A is right next to the B- but if it points to a D, then in order to obtain A* you really need to skip over the C sub-object, and since any given B* could point to a B or a D dynamically at run-time, then you will need to alter the pointer dynamically.现在,如果你有一个 B*,如果它真的指向一个 B,那么 A 就在 B 的旁边——但是如果它指向一个 D,那么为了获得 A*,你真的需要跳过 C sub -object,并且由于任何给定的B*都可以在运行时动态指向 B 或 D,因此您需要动态更改指针。 This, at the minimum, means that you will have to produce code to find that value by some means, as opposed to having the value baked-in at compile-time, which is what occurs for single inheritance.这至少意味着您将不得不通过某种方式生成代码以找到该值,而不是在编译时将值嵌入,这是单继承发生的情况。

At least in a typical implementation, virtual inheritance carries a (small!) penalty for (at least some) access to data members.至少在典型的实现中,虚拟继承对(至少一些)数据成员的访问会带来(小!)惩罚。 In particular, you normally end up with an extra level of indirection to access the data members of the object from which you've derived virtually.特别是,您通常会获得额外的间接级别来访问您从中虚拟派生的对象的数据成员。 This comes about because (at least in the normal case) two or more separate derived classes have not just the same base class, but the same base class object .这是因为(至少在正常情况下)两个或多个单独的派生类不仅具有相同的基类,而且具有相同的基类object To accomplish this, both of the derived classes have pointers to the same offset into the most derived object, and access those data members via that pointer.为了实现这一点,两个派生类都有指向最派生对象的相同偏移量的指针,并通过该指针访问这些数据成员。

Although it's technically not due to virtual inheritance, it's probably worth noting that there's a separate (again, small) penalty for multiple inheritance in general.尽管从技术上讲这不是由于虚拟继承,但可能值得注意的是,一般来说,多重继承有一个单独的(同样是小的)惩罚。 In a typical implementation of single inheritance, you have a vtable pointer at some fixed offset in the object (quite often the very beginning).继承的典型实现中,您在对象中的某个固定偏移处有一个 vtable 指针(通常是最开始的)。 In the case of multiple inheritance, you obviously can't have two vtable pointers at the same offset, so you end up with a number of vtable pointers, each at a separate offset in the object.在多重继承的情况下,显然不能在相同的偏移量处有两个 vtable 指针,因此最终会得到许多 vtable 指针,每个指针在对象中的单独偏移量处。

IOW, the vtable pointer with single inheritance is normally just static_cast<vtable_ptr_t>(object_address) , but with multiple inheritance you get static_cast<vtable_ptr_t>(object_address+offset) . IOW,具有单继承的 vtable 指针通常只是static_cast<vtable_ptr_t>(object_address) ,但是通过多重继承,您会得到static_cast<vtable_ptr_t>(object_address+offset)

Technically, the two are entirely separate -- but of course nearly the only use for virtual inheritance is in conjunction with multiple inheritance, so it's semi-relevant anyway.从技术上讲,两者是完全独立的——当然,虚拟继承几乎唯一的用途是与多重继承结合使用,所以无论如何它都是半相关的。

Concretely in Microsoft Visual C++ there is an actual difference in pointer-to-member sizes.具体而言,在 Microsoft Visual C++ 中,指向成员的指针大小存在实际差异。 See #pragma pointers_to_members .请参阅#pragma pointers_to_members As you can see in that listing - the most general method is "virtual inheritance" which is distinct from multiple inheritance which in turn is distinct from single inheritance.正如您在该清单中所看到的 - 最通用的方法是“虚拟继承”,它与多重继承不同,而多重继承又与单继承不同。

That implies that more information is needed to resolve a pointer-to-member in the case of presence of virtual inheritance, and it will have a performance impact if only through the amount of data taken up in the CPU cache - though likely also in the length of the lookup of the member or the number of jumps needed.这意味着在存在虚拟继承的情况下需要更多信息来解析指向成员的指针,并且如果仅通过 CPU 缓存中占用的数据量就会对性能产生影响 - 尽管也可能在成员查找的长度或所需的跳转次数。

Your question is focused mostly on calling regular functions of the virtual base, not the (far) more interesting case of virtual functions of the virtual base class (class A in your example)-- but yes, there can be a cost.您的问题主要集中在调用虚拟基类的常规函数上,而不是虚拟基类(在您的示例中为 A 类)的虚拟函数的(远)更有趣的情况——但是,是的,可能会有成本。 Of course everything is compiler dependent.当然,一切都依赖于编译器。

When the compiler compiled A::foo, it assumed that "this" points to the start of where the data members for A resides in memory.当编译器编译 A::foo 时,它假定“this”指向 A 的数据成员在内存中的起始位置。 At this time, the compiler might not know that class A will be a virtual base of any other class.此时,编译器可能不知道类 A 将是任何其他类的虚拟基类。 But it happily generates the code.但它很高兴地生成了代码。

Now, when the compiler compiles B, there won't really be a change because while A is a virtual base class, it is still single inheritance and in the typical case, the compiler will layout class B by placing class A's data members immediately followed by class B's data members-- so a B * can be immediately castable to a A * without any change in value, and hence, the no adjustments need to be made.现在,当编译器编译 B 时,实际上不会有任何变化,因为虽然 A 是虚拟基类,但它仍然是单继承的,在典型情况下,编译器将通过将类 A 的数据成员紧跟其后来布局类 B由 B 类的数据成员 - 因此 B * 可以立即转换为 A * 而不改变值,因此不需要进行任何调整。 The compiler can call A::foo using the same "this" pointer (even though it is of type B *) and there is no harm.编译器可以使用相同的“this”指针(即使它是 B * 类型)调用 A::foo 并且没有任何危害。

The same situation is for class C-- its still single inheritance, and the typical compiler will place A's data members immediately followed by C's data members so a C * can be immediately castable to an A * without any change in value.类 C 的情况相同——它仍然是单继承,典型的编译器将 A 的数据成员紧跟在 C 的数据成员之后放置,因此 C * 可以立即转换为 A * 而不改变任何值。 Thus, the compiler can simply call A::foo with the same "this" pointer (even though it is of type C*) and there is no harm.因此,编译器可以使用相同的“this”指针(即使它是 C* 类型)简单地调用 A::foo 并且没有任何危害。

However, the situation is totally different for class D. The layout of class D will typically be class A's data members, followed by class B's data members, followed by class C's data members, followed by class D's data members.但是,D 类的情况完全不同。D 类的布局通常是 A 类的数据成员,然后是 B 类的数据成员,然后是 C 类的数据成员,然后是 D 类的数据成员。

Using the typical layout, a D * can be immediately convertable to an A *, so there is no penalty for A::foo-- the compiler can call the same routine it generated for A::foo without any change to "this" and everything is fine.使用典型的布局,D * 可以立即转换为 A *,因此 A::foo 没有损失——编译器可以调用它为 A::foo 生成的相同例程,而无需对“this”进行任何更改一切都很好。

However, the situation changes if the compiler needs to call a member function such as C::other_member_func, even if C::other_member_func is non-virtual.但是,如果编译器需要调用诸如 C::other_member_func 之类的成员函数,即使 C::other_member_func 是非虚拟的,情况也会改变。 The reason is that when the compiler wrote the code for C::other_member_func, it assumed that the data layout referenced by the "this" pointer is A's data members immediately followed by C's data members.原因是当编译器为 C::other_member_func 编写代码时,它假定“this”指针引用的数据布局是 A 的数据成员,紧接着是 C 的数据成员。 But that is not true for an instance of D. The compiler may need to rewrite and create a (non-virtual) D::other_member_func, just to take care of the class instance memory layout difference.但对于 D 的实例则不然。编译器可能需要重写并创建一个(非虚拟的)D::other_member_func,只是为了处理类实例内存布局的差异。

Note that this is a different but similar situation when using multiple inheritance, but in multiple inheritance without virtual bases, the compiler can take care of everything by simply adding a displacement or fixup to the "this" pointer to account for where a base class is "embedded" within an instance of a derived class.请注意,当使用多重继承时,这是一种不同但相似的情况,但在没有虚基的多重继承中,编译器可以通过简单地向“this”指针添加位移或修正来解决所有问题,以说明基类的位置“嵌入”在派生类的实例中。 But with virtual bases, sometimes a function rewrite is needed.但是对于虚拟基础,有时需要重写函数。 It all depends on what data members are accessed by the (even non-virtual) member function being called.这一切都取决于被调用的(甚至是非虚拟的)成员函数访问了哪些数据成员。

For example, if class C defined a non-virtual member function C::some_member_func, the compiler might need to write:例如,如果类 C 定义了一个非虚成员函数 C::some_member_func,编译器可能需要这样写:

  1. C::some_member_func when called from an actual instance of C (and not D), as determined at compile time (because some_member_func isn't a virtual function) C::some_member_func 从 C 的实际实例(而不是 D)调用时,在编译时确定(因为 some_member_func 不是虚函数)
  2. C::some_member_func when the same member function is called from an actual instance of class D, as determined at compile time. C::some_member_func 当从类 D 的实际实例调用相同的成员函数时,在编译时确定。 (Technically this routine is D::some_member_func. Even though the definition of this member function is implicit and identical to the source code of C::some_member_func, the generated object code may be slightly different.) (从技术上讲,这个例程是 D::some_member_func。尽管这个成员函数的定义是隐式的,并且与 C::some_member_func 的源代码相同,但生成的目标代码可能略有不同。)

if the code for C::some_member_func happens to use member variables defined in both class A and class C.如果 C::some_member_func 的代码碰巧使用了在类 A 和类 C 中定义的成员变量。

I think, there is no runtime penalty for virtual inheritance.我认为,虚拟继承没有运行时惩罚。 Don't confuse virtual inheritance with virtual functions.不要将虚继承与虚函数混淆。 Both are two different things.两者是两种不同的东西。

virtual inheritance ensures that you've only one sub-object A in instances of D .虚拟继承确保您在D实例中只有一个子对象A So I don't think there would be runtime penalty for it alone .所以我不认为单独会有运行时惩罚。

However, there can arise cases where this sub-object cannot be known at compile time, so in such cases there would runtime penalty for virtual inheritance.但是,可能会出现在编译时无法知道此子对象的情况,因此在这种情况下,虚拟继承会导致运行时惩罚。 One such case is described by James in his answer.詹姆斯在他的回答中描述了一个这样的案例。

There has to be a cost to virtual-inheritance.虚拟继承必须有成本。

The proof is that virtually inherited classes occupy more than the sum of the parts.证明是虚拟继承的类占用的空间大于部分的总和。

Typical case:典型:

struct A{double a;};

struct B1 : virtual A{double b1;};
struct B2 : virtual A{double b2;};

struct C : virtual B1, virtual B2{double c;}; // I think these virtuals are not strictly necessary
static_assert( sizeof(A) == sizeof(double) ); // as expected

static_assert( sizeof(B1) > sizeof(A) + sizeof(double) ); // the equality holds for non-virtual inheritance
static_assert( sizeof(B2) > sizeof(A) + sizeof(double) );  // the equality holds for non-virtual inheritance
static_assert( sizeof(C) > sizeof(A) + sizeof(double) + sizeof(double) + sizeof(double) );
static_assert( sizeof(C) > sizeof(A) + sizeof(double) + sizeof(double) + sizeof(double) + sizeof(double));

( https://godbolt.org/z/zTcfoY ) ( https://godbolt.org/z/zTcfoY )

What is stored additionally?额外存储什么? I don't exactly understand.我不完全明白。 I think it is something like a virtual table but for accessing individual members.我认为它类似于虚拟表,但用于访问单个成员。

There is a cost of additional memory.有额外的 memory 成本。 For example, GCC 7 on x86-64 gives following results:例如,x86-64 上的 GCC 7 给出以下结果:

#include <iostream>

class A { int a; };
class B: public A { int b; };
class C: public A { int c; };
class D: public B, public C { int d; };
class BV: virtual public A { int b; };
class CV: virtual public A { int c; };
class DV: public BV, public CV { int d; };


int main()
{
    std::cout << sizeof(A) << std::endl;
    std::cout << sizeof(B) << std::endl;
    std::cout << sizeof(C) << std::endl;
    std::cout << sizeof(D) << std::endl;
    std::cout << sizeof(BV) << std::endl;
    std::cout << sizeof(CV) << std::endl;
    std::cout << sizeof(DV) << std::endl;
    return 0;
}

This prints out:这打印出来:

4
8
8
20
16
16
40

As you can see, some extra bytes added when you use virtual inheritance.如您所见,使用虚拟 inheritance 时添加了一些额外的字节。

Well, after many good answers explaining, while looking up the exact position of the virtual base class in memory incurs a performance penalty, there is a follow up question: "Can this penalty be reduced?"好吧,在许多好的答案解释之后,在 memory 中查找虚拟基地 class 的确切 position 会导致性能损失,有一个后续问题:“可以减少这种损失吗?” Fortunately, there is a partial solution in form of the (not yet mentioned) final keyword.幸运的是,有一个(尚未提及的) final关键字形式的部分解决方案。 In particular, calls from the class D of the original example to the innermost base A can usually be (almost) penalty-free, but in the general case only, if you final ize D .特别是,从原始示例的 class D到最里面的基数A的调用通常(几乎)没有惩罚,但仅在一般情况下,如果您final D

For why this is necessary, let's look at a multilevel class hierarchy:为什么这是必要的,让我们看一下多级 class 层次结构:

class Base {};

class ExtA : public virtual Base {};
class ExtB : public virtual Base {};
class ExtC : public virtual Base {};

class App1 : public Base {};
class App2 : public ExtA {};
class App3 : public ExtB, public ExtC {};

class SuperApp : public App2, public App3 {};

Because our App lication classes can use various of the Ext ension classes of our base class, none of those Ext ension classes can know at compile time, where the Base subobject will be located within the object, that they are called with.因为我们的App lication 类可以使用我们的基类 class 的各种Ext类,所以这些Ext类都无法在编译时知道Base子对象将位于 object 中的何处,它们将被调用。 Rather, they have to consult the virtual table at runtime to find out.相反,他们必须在运行时查询虚拟表才能找到答案。 This is, because the various Ext and App classes can all be defined in different translation units.这是因为各种ExtApp类都可以在不同的翻译单元中定义。

But the same problem exists for the App lication classes: Because App2 and App3 inherit a virtualized Base via the Ext ension class(es), they don't know at compile time, where that Base subobject is located within their own objects.但是App lication 类也存在同样的问题:因为App2App3通过Ext ension 类继承了一个虚拟化的Base ,所以它们在编译时不知道Base子对象在它们自己的对象中的位置。 So each method of App2 or App3 has to consult the virtual table to find the location of the Base subobject within their local objects.因此App2App3的每个方法都必须查询虚拟表以找到Base子对象在其本地对象中的位置。 This is, because it is syntactically legal to later combine those App classes further, as illustrated with the SuperApp class in the above hierarchy.这是因为稍后进一步组合这些App类在语法上是合法的,如上面层次结构中的SuperApp class 所示。

Also note, that there is a further penalty, if the Base class calls any virtual methods defined on the Ext ension or App lication level.另请注意,如果Base class 调用在Ext ension 或App lication 级别上定义的任何虚拟方法,则会有进一步的惩罚。 That's because the virtual method will be called with this pointing to a Base object, but they have to adjust this to the beginning of their own object by again consulting the virtual table.那是因为将调用虚方法时this指向一个Base object,但他们必须通过再次查询虚表将其调整为自己的 object 的开头。 If an Ext ension or App lication layer (virtual or non-virtual) method calls a virtual method defined on the Base class, that penalty is incurred twice: First for finding the Base subobject and then again for finding the real object relative from the Base subobject.如果ExtApp层(虚拟或非虚拟)方法调用在Base class 上定义的虚拟方法,则该惩罚会发生两次:第一次是为了找到Base子对象,然后再次是为了从Base找到真正的 object 相对对象子对象。

However, if we know, that a SuperApp combining several App s won't be created, we can improve things a lot by declaring the App classes final:但是,如果我们知道,不会创建结合多个AppSuperApp ,我们可以通过将App类声明为 final 来改进很多事情:

class App1 final : public Base {};
class App2 final : public ExtA {};
class App3 final : public ExtB, public ExtC {};

// class SuperApp : public App2, public App3 {};   // illegal now!

Because final makes the layout immutable, methods of the App lication classes don't need to go through a virtual table to find the Base subobject anymore.因为final使布局不可变, App lication 类的方法不再需要通过虚拟表 go 来查找Base子对象。 They just add the known constant offet to the this pointer, when calling any Base method.在调用任何Base方法时,它们只是将已知常量偏移量添加到this指针。 And virtual callbacks at the App lication layer can fixup the this pointer easily again by subtracting a constant known offset (or even not fix it up at all and reference the various fields from the middle of the object instead). App层的虚拟回调可以通过减去一个常量已知偏移量轻松地再次修复this指针(或者甚至根本不修复它,而是从 object 的中间引用各个字段)。 Methods of the Base class also don't incur any penalty upon themselves, because inside that class, everything works normal. Base class 的方法本身也不会受到任何惩罚,因为在 class 内部,一切正常。 So in this three-level scenario with final ized classes on the outmost level, only the execution of methods on the Ext ensions level is slower, if they need to refer to fields or methods of the Base class, or if they are virtually called from the Base .所以在这个最外层finalized classes的三层场景中,只有Ext final层的方法执行比较慢,如果他们需要引用Base class的字段或方法,或者如果他们是虚拟调用Base

The backdraw of the final keyword is, that it disallows all extensions. final关键字的缺点是,它不允许所有扩展。 You cannot derive an App2a from App2 anymore, even, if it doesn't require any of those Ext ensions.您不能再从App2派生App2a ,即使它不需要任何这些Ext And declaring a non- final App2Base and then final App2a and App2b from it, would again incur penalties for all the methods in App2Base , that refer to the original Base .并声明一个非finalApp2Base然后从中声明finalApp2aApp2b ,将再次对App2Base中引用原始Base的所有方法产生惩罚。 Unfortunately, the C++ Gods didn't give us a way to just unvirtualize a base class, but leave non-virtual extensions possible.不幸的是,C++ 大神们并没有给我们一个方法来对一个基数 class 进行非虚拟化,而是让非虚拟扩展成为可能。 They also didn't give us a way to declare a "master" Ext ension class, whose layout stays fixed, even if other Ext ensions with the same virtual Base class are also added (in this case, all the non-master Ext ensions would refer to the Base subobject within the master Ext ension).他们也没有给我们声明“主” Ext class 的方法,其布局保持固定,即使还添加了具有相同虚拟Base class 的其他Ext (在这种情况下,所有非主Ext将引用主Ext ension 中的Base子对象)。

The alternative to virtual inheritance like this is usually to add all the extension stuff to the Base class. Depending on the application, that might require a lot of extra and often unused fields and/or a lot of extra virtual method calls and/or a lot of dynamic_cast s, which all come with a performance penalty, too.虚拟 inheritance 的替代方法通常是将所有扩展内容添加到Base class。根据应用程序的不同,这可能需要大量额外且经常未使用的字段和/或大量额外的虚拟方法调用和/或很多dynamic_cast s,它们都会带来性能损失。

Also note, that in modern CPUs, the penalty after a mispredicted virtual function call is much higher than the penalty after a mispredicted this pointer fixup.另请注意,在现代 CPU 中,错误预测虚拟 function 调用后的惩罚高于错误预测this指针修正后的惩罚。 The first needs to throw away all results obtained on the wrong execution path and restart afresh on the right path.第一个需要丢弃在错误的执行路径上获得的所有结果,并在正确的路径上重新开始。 The later still needs to repeat all opcodes depending directly or indirectly on this , but doesn't need to load and decode instructions again.后者仍然需要重复直接或间接依赖于this的所有操作码,但不需要再次加载和解码指令。 BTW: The speculative execution with unknown pointer fixups is one of the reasons, why CPUs are vulnerable to Spectre/Meltdown type data leaks.顺便说一句:使用未知指针修复的推测执行是 CPU 易受 Spectre/Meltdown 类型数据泄漏影响的原因之一。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM