我提到了这个问题(我更改了它的标题)。我知道与virtual
ness相关的代码生成是特定于实现的。然而,前面的问题表明,在调用非虚拟基方法时,存在与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
或类指针,然后将其传递给原始方法。
如果你这样调整第三个例子:,你可能可以观察到头顶
- 将虚拟方法(非重叠名称)添加到Base、D1和D2。这将导致编译器创建虚拟方法表
- 将不重叠的数据字段/成员变量(不重叠,名称不同)添加到Base、D1、D2和派生
- 将对D2和Base中的数据字段进行操作的非虚拟方法添加到D2
- 将对D1和Base中的数据字段进行操作的非虚拟方法添加到D1
- 将调用D2和D1中上述非虚拟方法的非虚拟方法添加到Derived,然后对D2、D1、Base和Derived中的数据字段进行操作
- 调查拆卸情况
另外,一旦有了一些成员变量,您可能需要在调试器中研究派生类的布局。
当我检查它的组装时,所有3个版本之间没有区别
继承(虚拟或非虚拟)可能会增加一点差异,因为编译器可能会在将类的指针从Derived*转换为Base*(或者this
,如果从派生方法调用基本非虚拟方法)或vfptr时决定调整指针。这将导致在将值传递给函数/方法之前,向this
或指针的当前值添加一些值。
然而,这很可能会在调用函数调用时完成,并且很可能只在涉及多个继承时发生(因为可能有多个虚拟方法表)。也就是说,如果您使类C
继承类A
和B
,并且它们都有虚拟方法,但没有共同的祖先,那么当您从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
继承,编译器都知道如何转换它
您需要一个静态可用类型信息较少的示例来衡量虚拟继承的影响。