微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在从派生的立即对象中以非重写基方法间接调用虚拟方法时,是否执行vtable查找? 更多上下文

如何解决在从派生的立即对象中以非重写基方法间接调用虚拟方法时,是否执行vtable查找? 更多上下文

这个问题是this question一个稍微复杂的版本,已经很好地回答了。 在答案中使用的语义中,我指的是虚拟调用实现级别(即通常为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的查找吗?
  • 使用curiously recurring template pattern(CRTP)来实现静态多态性。

解决方法

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

B::generic_method是从A继承的,因此b.generic_method()是对A::generic_method的静态绑定(并且在此调用中,我上面已经讨论过specialized_method的动态绑定)

如果现代编译器可以“看到”对象的实际类型,则它们可以完全优化和跳过vtable查找(这种优化称为去虚拟化)。我所有的测试中,gcc都进行了虚拟化,而(在大多数情况下)无法听到c声。

使用-O3在gcc 10.2和clang 11.0.0上进行的所有测试。

情况:没有虚拟析构函数

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()

C语输出

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:

Gcc输出:

t1():
        sub     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,rbp
        call    [QWORD PTR [rax+16]]
        mov     rdi,r12
        call    _Unwind_Resume
t6():
        push    r12
        mov     edi,rax
        jmp     .L12
t6() [clone .cold]:
.L12:
        mov     rax,r12
        call    _Unwind_Resume
t7():
        push    rbp
        mov     edi,OFFSET FLAT:_ZTV1B+16
        mov     rbp,rax
        mov     rdi,rbp
        pop     rbp
        mov     rax,QWORD PTR [rax+16]
        jmp     rax
t8():
        push    rbp
        mov     edi,QWORD PTR [rax+16]
        jmp     rax

c声输出:

t1():                                 # @t1()
        push    rax
        mov     qword ptr [rsp],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],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,rbx
        call    qword ptr [rax + 16]
        mov     rdi,r14
        call    _Unwind_Resume
t6():                                 # @t6()
        push    r14
        push    rbx
        push    rax
        mov     edi,r14
        call    _Unwind_Resume
t7():                                 # @t7()
        push    rbx
        mov     edi,rbx
        pop     rbx
        jmp     qword ptr [rax + 16]            # TAILCALL
t8():                                 # @t8()
        push    rbx
        mov     edi,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
,

generic_method()函数具有隐式的A* const this参数,因此您误以为没有涉及指针或引用。该函数的主体不知道谁在调用它,因此它需要进行vtable查找以找出要调用的specialized_method()函数。

想象一下A是在某些库中定义的,我编写了一个新的C类,该类派生自A

如何将已经被编译并刚刚链接到我的程序中的库知道需要调用C::specialized_method()

理论上,编译器可能有足够的知识知道generic_method()仅在B对象上被调用过,或者它可以为每次调用内联generic_method(),但您不应这样做依靠。除了最简单的示例之外,它不太可能做到这一点。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。