当从派生的立即对象间接调用非重写基方法中的虚拟方法时,是否执行vtable查找

Is a vtable lookup performed when indirectly calling a virtual method in a non-overridden base method from a derived immediate object?

本文关键字:方法 虚拟 是否 查找 vtable 执行 派生 对象 调用 重写      更新时间:2024-09-29

这个问题是这个问题的一个稍微复杂一些的版本,已经得到了很好的回答。在答案中使用的语义中,我指的是虚拟调用的实现级别(即通常的vtable查找(。

基类使用在某些派生类中重写的虚拟成员来实现成员(通常是非虚拟的(。当使用派生的立即对象(不涉及指针或引用(来调用此成员时,是否涉及vtable查找?

以下是我想到的最简单的场景:

class A
{
public:
void generic_method()
{
// Do some stuff
specialized_method();
}
virtual void specialized_method(); // Details are useless here.
};
class B : public A
{
public:
void specialized_method() override;
};
int main()
{
// I don't want neither need type resolution at runtime.
// Using the immediate object
B b;
// Is there a vtable lookup for the indirect `specialized_method` call here?
b.generic_method();
}

涉及的每个类型都可以在编译时解析。因此,在这种情况下,我期待着直接调用,但我是否以某种方式阻止了这样的优化?

更多上下文

我不喜欢在我的用例中依赖编译器优化。以下是我正在努力实现的目标。任何建议都是受欢迎的。

  • generic_method是一种算法
  • 这依赖于某个容器的初始化。在这里,我希望用户可以完全自由地设置每个值。但是我想保证容器的结构是有效的
  • 因此,我将初始化母类中的结构(这不是微不足道的(,并让用户重写返回每个值(代码中的specific_method(的函数来填充容器。它将用于每个容器入口
  • 这个容器通常非常大,而且特定的方法非常快,所以虚拟调用开销是相关的

我的想法:

  • 不要依赖继承,而是使用lambda表达式作为参数。但是this不会被捕获,我不想更改访问说明符
  • 我可以把初始化这个容器的责任交给每个子类的构造函数。但我不能强制执行,也不能在代码中真正明确:我必须依靠文档来激励程序员用户以我想要的安全方式初始化容器
  • 使用纯虚拟方法作为专用方法会在编译时强制类型的解析并防止vtable查找吗
  • 使用奇怪的重复模板模式(CRTP(来实现静态多态性

A::generic_method中:由于specialized_method();实际上是this->specialized_method();,因此将执行动态绑定(例如vtable查找(。

B::generic_method是从A继承的,所以b.generic_method()是到A::generic_method的静态绑定(在这个调用中有我上面讨论过的specialized_method的动态绑定(

现代编译器可以优化并完全跳过vtable查找,如果它们可以";参见";对象的真实类型(优化称为去机会化(。gcc在我所有的测试中都表现出了失误,而(令我惊讶的是(clang在大多数情况下都不能。

用CCD_ 11对gcc 10.2和clang 11.0.0进行了各项试验。

情况:没有虚拟析构函数

导螺杆连杆

情况1:可以跳过vtable查找

auto t1()
{
B b{};
b.generic_method();
}
auto t2(B b)
{
b.generic_method();
}

对于t1,gcc和clang都跳过vtable查找,直接调用B::specialized_method()。对于t2,只有gcc执行优化:

gcc输出:

t1():
sub     rsp, 24
mov     QWORD PTR [rsp+8], OFFSET FLAT:_ZTV1B+16
lea     rdi, [rsp+8]
call    B::specialized_method()
add     rsp, 24
ret
t2(B):
jmp     B::specialized_method()

Clang输出

t1():                                 # @t1()
push    rax
mov     qword ptr [rsp], offset vtable for B+16
mov     rdi, rsp
call    B::specialized_method()
pop     rax
ret
t2(B):                                # @t2(B)
mov     rax, qword ptr [rdi]
jmp     qword ptr [rax]                 # TAILCALL

情况2:绑定必须是动态的,不能被破坏:

auto t3(B& b)
{
b.generic_method();
}
auto t4(B* b)
{
b->generic_method();
}
t3(B&):                               # @t3(B&)
mov     rax, qword ptr [rdi]
jmp     qword ptr [rax]                 # TAILCALL
t4(B*):                               # @t4(B*)
mov     rax, qword ptr [rdi]
jmp     qword ptr [rax]                 # TAILCALL

情境:虚拟析构函数

导螺杆连杆

情况1:可以跳过vtable查找

auto t1()
{
B b{};
b.generic_method();
}
auto t2(B b)
{
b.generic_method();
}
auto t5()
{
std::unique_ptr<B> b = std::make_unique<B>();
b->generic_method();
}

auto t6()
{
std::unique_ptr<A> b = std::make_unique<B>();
b->generic_method();
}
auto t7()
{
B* b = new B{};
b->generic_method();
delete b;
}
auto t8()
{
A* b = new B{};
b->generic_method();
delete b;
}

Gcc对所有的例子都执行去机会化,而clang对任何例子都不执行:

Gcc输出:

t1():
sub     rsp, 24
mov     QWORD PTR [rsp+8], OFFSET FLAT:_ZTV1B+16
lea     rdi, [rsp+8]
call    B::specialized_method()
add     rsp, 24
ret
t2(B):
jmp     B::specialized_method()
t5():
push    r12
mov     edi, 8
push    rbp
sub     rsp, 8
call    operator new(unsigned long)
mov     QWORD PTR [rax], OFFSET FLAT:_ZTV1B+16
mov     rdi, rax
mov     rbp, rax
call    B::specialized_method()
mov     rax, QWORD PTR [rbp+0]
mov     rdi, rbp
mov     rax, QWORD PTR [rax+16]
add     rsp, 8
pop     rbp
pop     r12
jmp     rax
mov     r12, rax
jmp     .L8
t5() [clone .cold]:
.L8:
mov     rax, QWORD PTR [rbp+0]
mov     rdi, rbp
call    [QWORD PTR [rax+16]]
mov     rdi, r12
call    _Unwind_Resume
t6():
push    r12
mov     edi, 8
push    rbp
sub     rsp, 8
call    operator new(unsigned long)
mov     QWORD PTR [rax], OFFSET FLAT:_ZTV1B+16
mov     rdi, rax
mov     rbp, rax
call    B::specialized_method()
mov     rax, QWORD PTR [rbp+0]
mov     rdi, rbp
mov     rax, QWORD PTR [rax+16]
add     rsp, 8
pop     rbp
pop     r12
jmp     rax
mov     r12, rax
jmp     .L12
t6() [clone .cold]:
.L12:
mov     rax, QWORD PTR [rbp+0]
mov     rdi, rbp
call    [QWORD PTR [rax+16]]
mov     rdi, r12
call    _Unwind_Resume
t7():
push    rbp
mov     edi, 8
call    operator new(unsigned long)
mov     QWORD PTR [rax], OFFSET FLAT:_ZTV1B+16
mov     rbp, rax
mov     rdi, rax
call    B::specialized_method()
mov     rax, QWORD PTR [rbp+0]
mov     rdi, rbp
pop     rbp
mov     rax, QWORD PTR [rax+16]
jmp     rax
t8():
push    rbp
mov     edi, 8
call    operator new(unsigned long)
mov     QWORD PTR [rax], OFFSET FLAT:_ZTV1B+16
mov     rbp, rax
mov     rdi, rax
call    B::specialized_method()
mov     rax, QWORD PTR [rbp+0]
mov     rdi, rbp
pop     rbp
mov     rax, QWORD PTR [rax+16]
jmp     rax

叮当声输出:

t1():                                 # @t1()
push    rax
mov     qword ptr [rsp], offset vtable for B+16
mov     rdi, rsp
call    qword ptr [rip + vtable for B+16]
pop     rax
ret
t2(B):                                # @t2(B)
mov     rax, qword ptr [rdi]
jmp     qword ptr [rax]                 # TAILCALL
t5():                                 # @t5()
push    r14
push    rbx
push    rax
mov     edi, 8
call    operator new(unsigned long)
mov     rbx, rax
mov     qword ptr [rax], offset vtable for B+16
mov     rdi, rax
call    qword ptr [rip + vtable for B+16]
mov     rax, qword ptr [rbx]
mov     rdi, rbx
add     rsp, 8
pop     rbx
pop     r14
jmp     qword ptr [rax + 16]            # TAILCALL
mov     r14, rax
mov     rax, qword ptr [rbx]
mov     rdi, rbx
call    qword ptr [rax + 16]
mov     rdi, r14
call    _Unwind_Resume
t6():                                 # @t6()
push    r14
push    rbx
push    rax
mov     edi, 8
call    operator new(unsigned long)
mov     rbx, rax
mov     qword ptr [rax], offset vtable for B+16
mov     rdi, rax
call    qword ptr [rip + vtable for B+16]
mov     rax, qword ptr [rbx]
mov     rdi, rbx
add     rsp, 8
pop     rbx
pop     r14
jmp     qword ptr [rax + 16]            # TAILCALL
mov     r14, rax
mov     rax, qword ptr [rbx]
mov     rdi, rbx
call    qword ptr [rax + 16]
mov     rdi, r14
call    _Unwind_Resume
t7():                                 # @t7()
push    rbx
mov     edi, 8
call    operator new(unsigned long)
mov     rbx, rax
mov     qword ptr [rax], offset vtable for B+16
mov     rdi, rax
call    qword ptr [rip + vtable for B+16]
mov     rax, qword ptr [rbx]
mov     rdi, rbx
pop     rbx
jmp     qword ptr [rax + 16]            # TAILCALL
t8():                                 # @t8()
push    rbx
mov     edi, 8
call    operator new(unsigned long)
mov     rbx, rax
mov     qword ptr [rax], offset vtable for B+16
mov     rdi, rax
call    qword ptr [rip + vtable for B+16]
mov     rax, qword ptr [rbx]
mov     rdi, rbx
pop     rbx
jmp     qword ptr [rax + 16]            # TAILCALL

情况2:绑定必须是动态的,不能被破坏:

auto t3(B& b)
{
b.generic_method();
}
auto t4(B* b)
{
b->generic_method();
}
t3(B&):                               # @t3(B&)
mov     rax, qword ptr [rdi]
jmp     qword ptr [rax]                 # TAILCALL
t4(B*):                               # @t4(B*)
mov     rax, qword ptr [rdi]
jmp     qword ptr [rax]                 # TAILCALL

generic_method()函数有一个隐式A* const this参数,因此您错误地认为没有涉及指针或引用。函数的主体不知道是谁在调用它,所以它需要进行vtable查找,以确定要调用哪个specialized_method()函数。

想象一下A是在某个库中定义的,我编写了一个从A派生的新的C类。这个库已经编译好了,正被链接到我的程序中,它怎么知道它需要调用C::specialized_method()

理论上,编译器可能有足够的知识来知道generic_method()只在B对象上被调用,或者它可以为每次调用内联generic_method(),但你不应该依赖于此。除了最简单的例子之外,它也不太可能做到这一点。