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

Posted

技术标签:

【中文标题】调用非虚拟基方法时,C++ 中的虚拟继承是不是有任何惩罚/成本?【英文标题】:Is there any penalty/cost of virtual inheritance in C++, when calling non-virtual base method?调用非虚拟基方法时,C++ 中的虚拟继承是否有任何惩罚/成本? 【发布时间】:2011-07-30 01:24:03 【问题描述】:

当我们从其基类调用 常规函数 成员时,在 C++ 中使用虚拟继承是否会在编译代码中产生运行时损失?示例代码:

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 ();

【问题讨论】:

这不是一个有趣的例子。由于 bar.foo() 的成本是固定的(因为该方法不是虚拟的)并且不会随着继承虚拟或其他方式而改变。 当您比较虚拟和非虚拟函数之间的代码生成时|(基类),请确保编译器不知道动态类型是什么 由于 bar.foo() 的成本是固定的(因为方法不是虚拟的)”,这是不正确的。动态类型在这里是已知的,因此虚拟性是无关紧要的。 foo 也可以是虚拟的。 【参考方案1】:

是的,如果您通过指针或引用调用成员函数并且编译器无法绝对确定该指针或引用指向或引用的对象类型,则可能存在。例如,考虑:

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

void g()

    D bar;
    f(&bar);

假设对f的调用没有内联,编译器需要生成代码来找到A虚拟基类子对象的位置,以便调用foo。通常这种查找涉及检查 vptr/vtable。

如果编译器知道您调用函数的对象的类型(如您的示例中的情况),则应该没有开销,因为函数调用可以静态分派(在编译时)。在您的示例中,bar 的动态类型已知为D(不能是其他任何东西),因此可以在编译时计算虚拟基类子对象A 的偏移量。

【讨论】:

@James:我认为您将虚拟继承与虚拟函数混淆了,不是吗? @Nawaz:不。给定一个B* 到一个可能是某个派生类的子对象的任意B 对象,您知道A 基类子对象相对于B* 的位置吗?在编译时,您不知道A 基类子对象在哪里。 @James:那么编译是如何决定的呢? @Nawaz:编译器不做任何决定:它生成代码以通过 vptr 或查找表进行 thunk,或使用其他特定于实现的方法来定位基类子对象。 @Martin:我的理解是你只需要类似于A* a_ptr = b_ptr + b_ptr->vtable[index_of_offset_of_virtual_base_A] 的东西(概念上)。对于给定的派生类(如D),A 子对象位于距B 子对象的已知偏移处。但是,如果您有一个 B* 指向某个任意对象,您不知道 相对于该指针 A 子对象在哪里而不检查 vtable,因为最派生的类型可能是 @ 987654339@ 或 D[something else]。 (我可能在这里表达得不够清楚;我今天早上才刚喝咖啡。)【参考方案2】:

是的,虚拟继承具有运行时性能开销。这是因为对于对象的任何指针/引用,编译器在编译时都找不到它的子对象。相反,对于单继承,每个子对象都位于原始对象的静态偏移处。考虑:

class A  ... ;
class B : public A  ... 

B的内存布局有点像这样:

| B's stuff | A's stuff |

在这种情况下,编译器知道 A 在哪里。但是,现在考虑 MVI 的情况。

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

B的内存布局:

| B's stuff | A's stuff |

C的内存布局:

| C's stuff | A's stuff |

但是等等!当 D 被实例化时,它看起来不是那样的。

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

现在,如果你有一个 B*,如果它真的指向 B,那么 A 就在 B- 旁边,但如果它指向 D,那么为了获得 A*,你真的需要跳过C 子对象,并且由于任何给定的B* 可以在运行时动态指向 B 或 D,因此您需要动态更改指针。这至少意味着您必须生成代码以通过某种方式找到该值,而不是在编译时将值嵌入,这就是单继承发生的情况。

【讨论】:

【参考方案3】:

至少在一个典型的实现中,虚拟继承对(至少一些)数据成员的访问带来了(很小的!)惩罚。特别是,您通常会以额外的间接级别来访问您虚拟派生的对象的数据成员。这是因为(至少在正常情况下)两个或多个独立的派生类不仅具有相同的基类,而且具有相同的基类object。为此,两个派生类都有指向最派生对象的相同偏移量的指针,并通过该指针访问这些数据成员。

虽然技术上不是由于虚拟继承,但可能值得注意的是,一般来说多重继承有一个单独的(同样,很小的)惩罚。在single 继承的典型实现中,您在对象的某个固定偏移处(通常是最开始)有一个vtable 指针。在多重继承的情况下,你显然不能有两个 vtable 指针在同一个偏移量,所以你最终会得到许多 vtable 指针,每个指针在对象中的一个单独的偏移量。

IOW,单继承的 vtable 指针通常只是 static_cast<vtable_ptr_t>(object_address),但如果是多继承,则得到 static_cast<vtable_ptr_t>(object_address+offset)

从技术上讲,这两者是完全独立的——当然,虚拟继承几乎唯一的用途是与多重继承结合使用,所以无论如何它是半相关的。

【讨论】:

【参考方案4】:

具体而言,在 Microsoft Visual C++ 中,指向成员的指针大小存在实际差异。 见#pragma pointers_to_members。正如您在该清单中看到的那样 - 最通用的方法是“虚拟继承”,它不同于多重继承,而多重继承又不同于单一继承。

这意味着在存在虚拟继承的情况下需要更多信息来解析指向成员的指针,如果仅通过 CPU 缓存中占用的数据量,它将对性能产生影响——尽管可能还在于成员查找的长度或所需的跳转次数。

【讨论】:

【参考方案5】:

我认为,虚拟继承没有运行时损失。 不要将虚继承与虚函数混淆。两者都是不同的东西。

虚拟继承确保您在D 的实例中只有一个子对象A。所以我认为单独不会对它造成运行时惩罚。

但是,可能会出现在编译时无法知道该子对象的情况,因此在这种情况下,虚拟继承会在运行时受到惩罚。 James 在他的回答中描述了一个这样的案例。

【讨论】:

其实可能有一个。在实际上从 A 继承的所有类中存储了一个额外的指针,用于定位这个子对象。然而,在 OP 的情况下,编译器可以静态确定 A 子对象所在的位置。 @Alexandre:很有趣。你能再解释一下吗?该实现是否已定义? 虚拟继承的 vptr 机制与虚拟方法相同。看看这篇文章:Solving the Diamond Problem with Virtual Inheritance EDIT:我的意思是,它使用虚拟指针,这可能会导致轻微的运行时损失。 可能这个答案是正确的。我已经提出了另一个question。投反对票并不完全合理。【参考方案6】:

您的问题主要集中在调用虚拟基类的 常规 函数,而不是虚拟基类(A 类)的 virtual 函数(远)更有趣的情况在您的示例中)-但是,是的,可能会产生成本。当然,一切都取决于编译器。

当编译器编译 A::foo 时,它假定“this”指向 A 的数据成员在内存中的起始位置。此时,编译器可能不知道类 A 将是任何其他类的虚拟基。但它很乐意生成代码。

现在,当编译器编译 B 时,实际上不会有任何变化,因为虽然 A 是一个虚拟基类,但它仍然是单继承,在典型情况下,编译器会通过放置类 A 的数据来布局类 B成员紧随其后的是 B 类的数据成员——因此 B * 可以立即转换为 A * 而值没有任何变化,因此不需要进行任何调整。编译器可以使用相同的“this”指针(即使它是 B * 类型)调用 A::foo,并且没有任何危害。

C 类也是同样的情况——它仍然是单继承,典型的编译器会将 A 的数据成员紧跟在 C 的数据成员之后,因此 C * 可以立即转换为 A * 而不会改变任何值。因此,编译器可以简单地使用相同的“this”指针(即使它是 C* 类型)调用 A::foo,并且没有任何危害。

但是,D 类的情况完全不同。D 类的布局通常是 A 类的数据成员,然后是 B 类的数据成员,然后是 C 类的数据成员,然后是 D 类的数据成员。

使用典型的布局,一个 D * 可以立即转换为一个 A *,因此对 A::foo 没有任何惩罚——编译器可以调用它为 A::foo 生成的相同例程,而无需对“这个”,一切都很好。

但是,如果编译器需要调用诸如 C::other_member_func 这样的成员函数,即使 C::other_member_func 是非虚函数,情况也会发生变化。原因是当编译器为 C::other_member_func 编写代码时,它假设“this”指针引用的数据布局是 A 的数据成员紧跟 C 的数据成员。但对于 D 的实例,情况并非如此。编译器可能需要重写并创建一个(非虚拟的)D::other_member_func,只是为了处理类实例内存布局的差异。

请注意,在使用多重继承时这是一种不同但相似的情况,但在没有虚基的多重继承中,编译器可以通过简单地向“this”指针添加位移或修正来解决所有问题基类“嵌入”在派生类的实例中。但是对于虚拟基础,有时需要重写函数。这完全取决于被调用的(甚至是非虚拟的)成员函数访问了哪些数据成员。

例如,如果类 C 定义了一个非虚拟成员函数 C::some_member_func,编译器可能需要这样写:

    C::some_member_func 从 C 的实际实例(而不是 D)调用时,在编译时确定(因为 some_member_func 不是虚函数) C::some_member_func 从 D 类的实际实例调用相同的成员函数,在编译时确定。 (从技术上讲,这个例程是 D::some_member_func。尽管这个成员函数的定义是隐式的并且与 C::some_member_func 的源代码相同,但生成的目标代码可能会略有不同。)

如果 C::some_member_func 的代码碰巧使用了在 A 类和 C 类中定义的成员变量。

【讨论】:

【参考方案7】:

虚拟继承是有代价的。

证明是虚拟继承的类比部分的总和还要多。

典型案例:

struct Adouble a;;

struct B1 : virtual Adouble b1;;
struct B2 : virtual Adouble b2;;

struct C : virtual B1, virtual B2double 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)

额外存储了什么?我不太明白。 我认为它类似于虚拟表,但用于访问单个成员。

【讨论】:

以上是关于调用非虚拟基方法时,C++ 中的虚拟继承是不是有任何惩罚/成本?的主要内容,如果未能解决你的问题,请参考以下文章

C ++中的非对称虚拟继承菱形

这。与基地。对于继承的受保护的非虚拟方法?

在使用 C++ 进行虚拟继承期间调用构造函数

C++ - 替换基类方法

关于C++中的虚拟继承的一些总结

非 Diamond 类型中的虚拟继承