在虚拟继承中调用非虚拟基方法是否有任何额外的成本

Is there any extra cost of calling non-virtual base methods in virtual inheritance?

本文关键字:虚拟 任何额 是否 方法 继承 调用      更新时间:2023-10-16

我提到了这个问题(我更改了它的标题)。我知道与virtualness相关的代码生成是特定于实现的。然而,前面的问题表明,在调用非虚拟基方法时,存在与virtual继承相关的额外成本。

我写了以下测试代码,并在g++中检查了它的组装(使用-O4):

公用部分

struct Base {
  int t_size;
  Base (int i) : t_size(i) {}
  virtual ~Base () {}
  int size () const { return t_size; };
};
struct D1 : virtual Base {
  int a[10];
  D1 () : Base(0) {}
  ~D1 () {}
};
struct D2 : virtual Base {
  int a[20];
  D2() : Base(0) {}
  ~D2 () {}
};
...
void foo (Base *p) 
{
  if(p->size())
    return;
  p = 0;
}
int main ()
{
  Derived d;
  foo(&d);
}

现在不同的部分在这里:

代码1(正常继承)

struct Derived : Base {
  Derived () : Base(0) {}
  ~Derived () {}
  int a[100];
};

代码2(虚拟继承)

struct Derived : virtual Base {
  Derived () : Base(0) {}
  ~Derived () {}
  int a[100];
};

代码3(多重虚拟继承)

struct Derived : D1, D2 {
  Derived () : Base(0) {}
  ~Derived () {}
  int a[100];
};

此处为总体代码。

当我检查它的程序集时,所有3个版本之间没有差异。以下是装配代码:

        .file   "virtualInheritFunctionCall.cpp"
        .text
        .p2align 4,,15
        .globl  _Z3fooP4Base
        .type   _Z3fooP4Base, @function
_Z3fooP4Base:
.LFB1:
        .cfi_startproc
        rep 
        ret 
        .cfi_endproc
.LFE1:
        .size   _Z3fooP4Base, .-_Z3fooP4Base
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB2:
        .cfi_startproc
        xorl    %eax, %eax
        ret 
        .cfi_endproc
.LFE2:
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1"
        .section        .note.GNU-stack,"",@progbits

这是否意味着当某些优化开启时,virtual继承没有任何额外的成本?我是否需要执行任何更复杂的测试代码来评估这一点?请注意,如果不进行优化,这些程序集之间存在差异。

与其说性能,我想知道虚拟继承是如何处理非虚拟基本方法的

显然,它会调整this或类指针,然后将其传递给原始方法。

如果你这样调整第三个例子:,你可能可以观察到头顶

  1. 将虚拟方法(非重叠名称)添加到Base、D1和D2。这将导致编译器创建虚拟方法表
  2. 将不重叠的数据字段/成员变量(不重叠,名称不同)添加到Base、D1、D2和派生
  3. 将对D2和Base中的数据字段进行操作的非虚拟方法添加到D2
  4. 将对D1和Base中的数据字段进行操作的非虚拟方法添加到D1
  5. 将调用D2和D1中上述非虚拟方法的非虚拟方法添加到Derived,然后对D2、D1、Base和Derived中的数据字段进行操作
  6. 调查拆卸情况

另外,一旦有了一些成员变量,您可能需要在调试器中研究派生类的布局。

当我检查它的组装时,所有3个版本之间没有区别

继承(虚拟或非虚拟)可能会增加一点差异,因为编译器可能会在将类的指针从Derived*转换为Base*(或者this,如果从派生方法调用基本非虚拟方法)或vfptr时决定调整指针。这将导致在将值传递给函数/方法之前,向this或指针的当前值添加一些值。

然而,这很可能会在调用函数调用时完成,并且很可能只在涉及多个继承时发生(因为可能有多个虚拟方法表)。也就是说,如果您使类C继承类AB,并且它们都有虚拟方法,但没有共同的祖先,那么当您从C调用属于A的方法时,您可能会在反汇编中看到指针调整。但仅此而已。这样的管理费用将低得离谱。

请注意,这是编译器特有的问题,我在这里所写的一切都是基于对微软编译器的观察。也就是说,它是"未记录的特性",因此,如果您担心性能,您应该使用探查器,而不是试图猜测性能影响。无论如何,首要任务应该是代码的可读性。

首先,看一下foo:

void foo (Base *p) 
{
  if(p->size())
    return;
  p = 0;
}

由于Base::size()是非虚拟的,因此p->size()不存在虚拟调度开销。

接下来,看看如何调用foo:

int main ()
{
  Derived d;
  foo(&d);
}

这里,您获取的是一个类型静态已知的实例的地址,即,给定一个Derived的实例,编译器可以静态地确定如何将其转换为Base *。因此,无论Derived如何从Base继承,编译器都知道如何转换它

您需要一个静态可用类型信息较少的示例来衡量虚拟继承的影响。