如何解决在从派生的立即对象中以非重写基方法间接调用虚拟方法时,是否执行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上进行的所有测试。
情况:没有虚拟析构函数
情况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
情况:虚拟析构函数
情况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 举报,一经查实,本站将立刻删除。