从派生立即数 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
是算法。
- 这依赖于一些容器的初始化。在这里,我希望用户可以完全自由地设置每个值。但是我想保证容器的结构是有效的。
- 因此,我会在母 class 中初始化结构 - 这不是微不足道的 - 并让用户覆盖返回每个值(代码中的
specific_method
)的函数以填充容器.它将用于每个容器条目。
- 这个容器一般很大,具体方法很快,所以虚拟调用开销是相关的。
我想到的:
- 不要依赖继承并使用 lambda 表达式作为参数。但是
this
不会被捕获,我不想更改访问说明符。
- 我可以将初始化此容器的责任交给每个 child class 的构造函数。但我无法强制执行,也无法在代码中使其真正明确:我将不得不依靠文档来激励 programmer-user 以我想要的安全方式初始化容器。
- 使用纯虚方法作为专用方法是否会在编译时强制解析类型并阻止 vtable 查找?
- 使用curiously recurring template pattern (CRTP) 实现静态多态。
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
。
情况:没有虚拟析构函数
案例 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 为 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
这个问题是
基础 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
是算法。- 这依赖于一些容器的初始化。在这里,我希望用户可以完全自由地设置每个值。但是我想保证容器的结构是有效的。
- 因此,我会在母 class 中初始化结构 - 这不是微不足道的 - 并让用户覆盖返回每个值(代码中的
specific_method
)的函数以填充容器.它将用于每个容器条目。 - 这个容器一般很大,具体方法很快,所以虚拟调用开销是相关的。
我想到的:
- 不要依赖继承并使用 lambda 表达式作为参数。但是
this
不会被捕获,我不想更改访问说明符。 - 我可以将初始化此容器的责任交给每个 child class 的构造函数。但我无法强制执行,也无法在代码中使其真正明确:我将不得不依靠文档来激励 programmer-user 以我想要的安全方式初始化容器。
- 使用纯虚方法作为专用方法是否会在编译时强制解析类型并阻止 vtable 查找?
- 使用curiously recurring template pattern (CRTP) 实现静态多态。
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
。
情况:没有虚拟析构函数
案例 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 为 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