在虚拟继承中调用非虚拟基本方法是否需要额外的成本?

时间:2012-02-25 12:32:31

标签: c++ optimization runtime overhead virtual-inheritance

我提到this question(我更改了标题)。我知道与virtual相关的代码生成是特定于实现的。但是,之前的问题表明,在调用非虚拟基本方法时,存在与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];
};

Overall code here

当我检查其装配时,所有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继承没有任何额外费用?我是否需要执行任何更复杂的测试代码来评估此问题?请注意,如果没有优化,这些程序集之间就会有所不同。

2 个答案:

答案 0 :(得分:3)

  

我想知道虚拟继承如何处理非虚拟基本方法

,而不是性能

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

如果你以这种方式调整第三个例子,你可能会观察到开销:

  1. 将虚拟方法(非重叠名称)添加到Base,D1和D2。这将导致编译器创建虚方法表。
  2. 将非重叠数据字段/成员变量(非重叠,不同名称)添加到Base,D1,D2并派生。
  3. 将非虚方法添加到D2,该方法对D2和Base中的数据字段进行操作。
  4. 将非虚方法添加到D1,该方法对D1和Base中的数据字段进行操作。
  5. 向Derived添加非虚拟方法,调用D2和D1中的上述非虚方法,然后对D2,D1,Base和Derived中的数据字段进行操作。
  6. 调查反汇编。
  7. 此外,一旦有一些成员变量,您可能希望调查调试器中生成的Derived类的布局。

      

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

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

    但是,这最有可能在调用函数调用时完成,并且只有在涉及多个继承时才会发生(因为可能存在多个虚方法表)。即如果你创建继承类CA的类B并且它们都有虚拟方法但没有共同的祖先,那么当你从{调用属于A的方法时{1}}您可能会在反汇编中看到指针调整。但就是这样。这种开销的成本将非常小。

    请注意,这是特定于编译器的问题,我在这里写的所有内容都是基于对microsoft编译器的观察。即它是“未记录的功能”,因此,如果您担心性能,则应使用分析器而不是尝试猜测性能影响。无论如何,主要优先级应该是代码可读性。

答案 1 :(得分:2)

首先,看看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继承,编译器都知道如何转换它。

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