拿这个愚蠢的例子:
class Base
{
public:
virtual void ant() { i++; };
virtual void dec() { i--; };
int i;
};
void function(Base * base)
{
base->ant();
base->dec();
}
我想象这是由编译器实现的方式是两个虚函数调用。 Clang就是这样做的(使用尾调用来调用dec()): -
function(Base*): # @function(Base*)
push rbx
mov rbx, rdi
mov rax, qword ptr [rbx]
call qword ptr [rax]
mov rax, qword ptr [rbx]
mov rdi, rbx
pop rbx
jmp qword ptr [rax + 8] # TAILCALL
另一方面,GCC远远超出了部分内联函数调用的方式:
Base::ant():
add DWORD PTR [rdi+8], 1 # this_2(D)->i,
ret
Base::dec():
sub DWORD PTR [rdi+8], 1 # this_2(D)->i,
ret
function(Base*):
push rbx #
mov rax, QWORD PTR [rdi] # _3, base_2(D)->_vptr.Base
mov rbx, rdi # base, base
mov rdx, QWORD PTR [rax] # _4, *_3
cmp rdx, OFFSET FLAT:Base::ant() # _4,
jne .L4 #,
mov rax, QWORD PTR [rax+8] # _7, MEM[(int (*__vtbl_ptr_type) () *)prephitmp_8 + 8B]
add DWORD PTR [rdi+8], 1 # base_2(D)->i,
cmp rax, OFFSET FLAT:Base::dec() # _7,
jne .L6 #,
.L12:
sub DWORD PTR [rbx+8], 1 # base_2(D)->i,
pop rbx #
ret
.L4:
call rdx # _4
mov rax, QWORD PTR [rbx] # _3, base_2(D)->_vptr.Base
mov rax, QWORD PTR [rax+8] # _7, MEM[(int (*__vtbl_ptr_type) () *)prephitmp_8 + 8B]
cmp rax, OFFSET FLAT:Base::dec() # _7,
je .L12 #,
.L6:
mov rdi, rbx #, base
pop rbx #
jmp rax # _7
这真的有利吗?为什么?它看起来像是相同数量的内存查找,但在混合中抛出了额外的比较。也许我的例子太琐碎了。
有关您可以使用的版本,请参阅godbolt。
答案 0 :(得分:2)
gcc假设两个可预测的分支比两个间接调用便宜。如果事实证明它猜对了,并且内联的调用是应该发生的调用,那肯定是正确的。配置文件引导优化(-fprofile-generate
/ -fprofile-use
)可能会检测是否不是这种情况,并且如果合适,推测性地内联其他内容。 (或者,也许不是那么聪明)。
间接分支可能非常昂贵,因为分支预测器必须正确预测目标地址。如果它们是可预测的,那么两个未采取的分支是非常便宜的。我有点惊讶gcc没有检查两个地址是否为基类,在这种情况下根本没有触及i
(因为inc和dec取消了)。 )
现代CPU非常擅长快速浏览大量指令,而在这种情况下有相当数量的指令级并行性。请参阅Agner Fog's microarch guide标记wiki中的x86和其他链接。