从派生立即数 object 间接调用 non-overridden 基方法中的虚拟方法时是否执行 vtable 查找?

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

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

基础 class 使用虚拟成员实现成员(通常是非虚拟成员),在某些派生 class 中被覆盖。使用派生立即数object(不涉及指针或引用)调用该成员时,是否涉及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() 函数有一个隐含的 A* const this 参数,所以你错了,没有涉及指针或引用。函数体不知道谁在调用它,因此它需要进行 vtable 查找以确定要调用哪个 specialized_method() 函数。

假设 A 在某个库中定义,我写了一个新的 C class 派生自 A。已经编译并且刚刚链接到我的程序中的库如何知道它需要调用 C::specialized_method()?

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

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

B::generic_method 继承自 A 所以 b.generic_method() 是对 A::generic_method 的静态绑定(在这个调用中有 specialized_method 的动态绑定我已经说过以上)

现代编译器可以优化并完全跳过 vtable 查找,如果它们可以“看到”对象的真实类型(优化称为去虚拟化)。 gcc 在我的所有测试中都执行了去虚拟化,而(令我惊讶的是)clang 在大多数情况下都无法执行。

gcc 10.2 和 clang 11.0.0 上的所有测试 -O3

情况:没有虚拟析构函数

godbolt link

案例 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

情况:虚析构函数

godbolt link

案例 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 为 none:

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

clang 输出:

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