我遇到了一个这样的问题,即“final”关键字如何可以用来减少虚拟方法的开销(Virtual function efficiency and the 'final' keyword)。基于此答案,期望的是派生类指针调用标有final的重写方法将不会面临动态分配的开销。
为了测试该方法的好处,我设置了一些示例类,并在Quick-Bench-Here is the link上运行了它。这里有3种情况:
情况1 :没有最终说明符的派生类指针:
Derived* f = new DerivedWithoutFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
情况2 :具有最终说明符的基类指针:
Base* f = new DerivedWithFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
情况3 :具有最终说明符的派生类指针:
Derived* f = new DerivedWithFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
这里的
run_multiple
函数如下所示:int run_multiple(int times) specifiers {
int sum = 0;
for(int i = 0; i < times; i++) {
sum += run_once();
}
return sum;
}
我观察到的结果是:
按速度:情况2 ==情况3>情况1
但是,情况3的速度是否应该比情况2快得多?我的实验设计或对预期结果的假设是否有问题?
编辑:
彼得·科德斯(Peter Cordes)指出了一些非常有用的文章,可供您进一步阅读与该主题相关的文章:
Is final used for optimization in C++?
Why can't gcc devirtualize this function call?
LTO, Devirtualization, and Virtual Tables
最佳答案
您已正确理解final
的效果(情况2的内部循环除外),但您的成本估算却遥遥无期。我们不应该期望在任何地方产生大的影响,因为mt19937只是速度很慢,并且所有3个版本都在其中花费了大量时间。
唯一不会丢失/掩埋在噪声/开销中的事情是将int run_once() override final
内联到FooPlus::run_multiple
的内部循环中的效果,情况2和情况3均会运行该效果。
但是情况1无法将Foo::run_once()
内联到Foo::run_multiple()
中,因此与其他2种情况不同,内部循环内部存在函数调用开销。
情况2必须重复调用run_multiple
,但是每100次run_once
运行仅一次,并且没有可测量的效果。
对于所有3种情况,大部分时间都用在dist(rng);
上,因为与不内联函数调用的额外开销相比,std::mt19937
相当慢。乱序执行也可能会隐藏很多开销。但并非全部,因此仍有一些要衡量的地方。
情况3能够将所有内容都内联到该asm循环中(来自您的quickbench链接):
# percentages are *self* time, not including time spent in the PRNG
# These are from QuickBench's perf report tab,
# presumably sample for core clock cycle perf events.
# Take them with a grain of salt: superscalar + out-of-order exec
# makes it hard to blame one instruction for a clock cycle
VirtualWithFinalCase2(benchmark::State&): # case 3 from QuickBench link
... setup before the loop
.p2align 3
.Louter: # do{
xor %ebp,%ebp # sum = 0
mov $0x64,%ebx # inner = 100
.p2align 3 # nopw 0x0(%rax,%rax,1)
.Linner: # do {
51.82% mov %r13,%rdi
mov %r15,%rsi
mov %r13,%rdx # copy args from call-preserved regs
callq 404d60 # mt PRNG for unsigned long
47.27% add %eax,%ebp # sum += run_once()
add $0xffffffff,%ebx # --inner
jne .Linner # }while(inner);
mov %ebp,0x4(%rsp) # store to volatile local: benchmark::DoNotOptimize(x);
0.91% add $0xffffffffffffffff,%r12 # --outer
jne # } while(outer)
情况2仍可以将
run_once
内联到run_multiple
中,因为class FooPlus
使用int run_once() override final
。外循环中只有虚拟调度开销(仅),但是每次外循环迭代所产生的少量额外费用与内循环的成本(在情况2和情况3之间相同)完全相形见war。因此,内部循环本质上是相同的,仅在外部循环中具有间接调用开销。毫不奇怪,这是无法测量的,或者至少在Quickbench上的噪声中消失了。
情况1无法将
Foo::run_once()
内联到Foo::run_multiple()
中,因此也存在函数调用开销。 (它是间接函数调用的事实相对较小;在紧密循环中,分支预测将完成近乎完美的工作。)如果您查看Quick-Bench链接上的反汇编,则情况1和情况2的外循环具有相同的asm。
没有人可以取消虚拟化并内联
run_multiple
。情况1,因为它是虚拟的非最终值,情况2,因为它只是基类,而不是具有final
覆盖的派生类。 # case 2 and case 1 *outer* loops
.loop: # do {
mov (%r15),%rax # load vtable pointer
mov $0x64,%esi # first C++ arg
mov %r15,%rdi # this pointer = hidden first arg
callq *0x8(%rax) # memory-indirect call through a vtable entry
mov %eax,0x4(%rsp) # store the return value to a `volatile` local
add $0xffffffffffffffff,%rbx
jne 4049f0 .loop # } while(--i != 0);
这可能是错过的优化:编译器可以证明
Base *f
来自new FooPlus()
,因此从静态上已知其类型为FooPlus
。可以重写operator new
,但是编译器仍会发出对FooPlus::FooPlus()
的单独调用(向其传递new
指向存储的指针)。因此,这似乎只是在案例2和案例1中没有利用优势。关于c++ - 使用final减少虚拟方法的开销,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/53603443/