我遇到了一个这样的问题,即“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/

10-10 21:26