使用final来减少虚拟方法开销



我遇到了一个SO问题,关于如何使用"final"关键字来减少虚拟方法开销(虚拟函数效率和';final';关键字(。基于这个答案,期望调用标有final的overriden方法的派生类指针不会面临动态调度的开销。

为了对这种方法的优点进行基准测试,我设置了一些示例类,并在Quick Bench上运行了它——这是链接。这里有三种情况:
情况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指出了一些与此主题相关的非常有用的文章供进一步阅读:
final是否用于C++中的优化
为什么可以';gcc是否取消了此函数调用的机会
LTO、虚拟化和虚拟表

您正确地理解了final的影响(可能除了案例2的内部循环(,但您的成本估计还很远。我们不应该期望在任何地方产生大的影响,因为mt19937非常慢,所有三个版本都花了大部分时间。


唯一没有丢失/隐藏在噪声/开销中的是将int run_once() override final内联到FooPlus::run_multiple中的内部循环中的效果,案例2和案例3都运行该循环

但是案例1无法将Foo::run_once()内联到Foo::run_multiple()中,因此与其他两种情况不同,内部循环中存在函数调用开销

情况2必须重复调用run_multiple,但这只是每100次运行run_once一次,并且没有可测量的效果。


对于所有这三种情况,大部分时间都花在了dist(rng);上,因为与不内联函数调用的额外开销相比,std::mt19937相当慢。无序执行可能也会隐藏很多开销。但并不是全部,所以还有一些东西需要衡量。

案例3能够将所有内容内联到此asm循环(从您的快速工作台链接(:

# 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相同(。

因此,内部循环将基本相同,间接调用开销仅在外部循环中。毫不奇怪,这是无法测量的,或者至少在Quickbench上的噪音中丢失了。


案例1无法将Foo::run_once()内联到Foo::run_multiple()中,因此也存在函数调用开销。(这是一个间接函数调用的事实相对较小;在紧密循环中,分支预测将做得近乎完美。(


如果您查看快速工作台链接上的拆卸,案例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(),因此静态地已知其类型为FooPlusoperator new可以被重写,但编译器仍然会发出对FooPlus::FooPlus()的单独调用(从new向它传递一个指向存储器的指针(。因此,这似乎只是一种叮当声,在案例2和案例1中都没有利用。

相关内容

  • 没有找到相关文章

最新更新