我稍微修改了代码以自己尝试一下,在我看来,它似乎正在删除 vtable,但我在 asm 方面不够专业,无法判断。我敢肯定,一些评论员会让我正确:)
struct A {
virtual int foo() { return 1; }
};
struct B : public A {
virtual int foo() { return 2; }
};
int useIt(A* a) {
return a->foo();
}
int main()
{
B* b = new B();
return useIt(b);
}
然后我将这段代码转换成这样的程序集:
g++ -g -S -O0 -fverbose-asm virt.cpp
as -alhnd virt.s > virt.base.asm
g++ -g -S -O6 -fverbose-asm virt.cpp
as -alhnd virt.s > virt.opt.asm
在我看来,有趣的部分就像“选择”版本正在删除 vtable。看起来它正在创建 vtable 但没有使用它..
在 opt asm 中:
9:virt.cpp **** int useIt(A* a) {
89 .loc 1 9 0
90 .cfi_startproc
91 .LVL2:
10:virt.cpp **** return a->foo();
92 .loc 1 10 0
93 0000 488B07 movq (%rdi), %rax # a_1(D)->_vptr.A, a_1(D)->_vptr.A
94 0003 488B00 movq (%rax), %rax # *D.2259_2, *D.2259_2
95 0006 FFE0 jmp *%rax # *D.2259_2
96 .LVL3:
97 .cfi_endproc
和base.asm版本一样:
9:virt.cpp **** int useIt(A* a) {
88 .loc 1 9 0
89 .cfi_startproc
90 0000 55 pushq %rbp #
91 .LCFI6:
92 .cfi_def_cfa_offset 16
93 .cfi_offset 6, -16
94 0001 4889E5 movq %rsp, %rbp #,
95 .LCFI7:
96 .cfi_def_cfa_register 6
97 0004 4883EC10 subq $16, %rsp #,
98 0008 48897DF8 movq %rdi, -8(%rbp) # a, a
10:virt.cpp **** return a->foo();
99 .loc 1 10 0
100 000c 488B45F8 movq -8(%rbp), %rax # a, tmp64
101 0010 488B00 movq (%rax), %rax # a_1(D)->_vptr.A, D.2263
102 0013 488B00 movq (%rax), %rax # *D.2263_2, D.2264
103 0016 488B55F8 movq -8(%rbp), %rdx # a, tmp65
104 001a 4889D7 movq %rdx, %rdi # tmp65,
105 001d FFD0 call *%rax # D.2264
11:virt.cpp **** }
106 .loc 1 11 0
107 001f C9 leave
108 .LCFI8:
109 .cfi_def_cfa 7, 8
110 0020 C3 ret
111 .cfi_endproc
在第 93 行,我们在 cmets 中看到:_vptr.A 我很确定这意味着它正在执行 vtable 查找,但是,在实际的 main 函数中,它似乎能够预测答案,甚至无法预测调用该 useIt 代码:
16:virt.cpp **** return useIt(b);
17:virt.cpp **** }
124 .loc 1 17 0
125 0015 B8020000 movl $2, %eax #,
我认为这只是说,我们知道我们会返回 2,让我们把它放在 eax 中。 (我重新运行程序要求它返回 200,并且该行已按我的预期更新)。
额外的一点
所以我把程序复杂了一点:
struct A {
int valA;
A(int value) : valA(value) {}
virtual int foo() { return valA; }
};
struct B : public A {
int valB;
B(int value) : valB(value), A(0) {}
virtual int foo() { return valB; }
};
int useIt(A* a) {
return a->foo();
}
int main()
{
A* a = new A(100);
B* b = new B(200);
int valA = useIt(a);
int valB = useIt(a);
return valA + valB;
}
在这个版本中,useIt代码肯定使用了优化程序集中的vtable:
13:virt.cpp **** int useIt(A* a) {
89 .loc 1 13 0
90 .cfi_startproc
91 .LVL2:
14:virt.cpp **** return a->foo();
92 .loc 1 14 0
93 0000 488B07 movq (%rdi), %rax # a_1(D)->_vptr.A, a_1(D)->_vptr.A
94 0003 488B00 movq (%rax), %rax # *D.2274_2, *D.2274_2
95 0006 FFE0 jmp *%rax # *D.2274_2
96 .LVL3:
97 .cfi_endproc
这一次,main 函数内联了useIt 的副本,但实际上执行了 vtable 查找。
c++11 和 'final' 关键字呢?
所以我把一行改成:
virtual int foo() override final { return valB; }
和编译器行:
g++ -std=c++11 -g -S -O6 -fverbose-asm virt.cpp
认为告诉编译器这是一个最终覆盖,可能会允许它跳过 vtable。
原来它仍然使用 vtable。
所以我的理论答案是:
- 我认为没有任何明确的“不要使用 vtable”优化。 (我在 g++ 手册页中搜索了 vtable 和 virt 等,但一无所获)。
- 但是带有 -O6 的 g++ 可以对具有明显常量的简单程序进行大量优化,从而可以预测结果并完全跳过调用。
- 但是,一旦事情变得复杂(读起来),它肯定会进行 vtable 查找,几乎每次调用虚函数时。