Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/oop/2.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C++ 使用final减少虚拟方法开销_C++_Oop_Benchmarking - Fatal编程技术网

C++ 使用final减少虚拟方法开销

C++ 使用final减少虚拟方法开销,c++,oop,benchmarking,C++,Oop,Benchmarking,我遇到了这样一个问题:如何使用“final”关键字来减少虚拟方法开销()。基于这个答案,我们期望调用用final标记的重写方法的派生类指针不会面临动态调度的开销 为了对这种方法的优点进行基准测试,我设置了几个示例类并在Quick Bench上运行它。这里有3个案例: 案例1:不带最终说明符的派生类指针: Derived* f = new DerivedWithoutFinalSpecifier(); f->run_multiple(100); // calls an overriden m

我遇到了这样一个问题:如何使用“final”关键字来减少虚拟方法开销()。基于这个答案,我们期望调用用final标记的重写方法的派生类指针不会面临动态调度的开销

为了对这种方法的优点进行基准测试,我设置了几个示例类并在Quick Bench上运行它。这里有3个案例:
案例1:不带最终说明符的派生类指针:

Derived* f = new DerivedWithoutFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
Base* f = new DerivedWithFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
Derived* f = new DerivedWithFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
案例2:带有最终说明符的基类指针:

Derived* f = new DerivedWithoutFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
Base* f = new DerivedWithFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
Derived* f = new DerivedWithFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
案例3:带有最终说明符的派生类指针:

Derived* f = new DerivedWithoutFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
Base* f = new DerivedWithFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
Derived* f = new DerivedWithFinalSpecifier();
f->run_multiple(100); // calls an overriden method 100 times
这里的函数
run\u multiple
如下所示:

int run_multiple(int times) specifiers {
    int sum = 0;
    for(int i = 0; i < times; i++) {
        sum += run_once();
    }
    return sum;
}
int运行\u多个(int次)说明符{
整数和=0;
for(int i=0;i
我观察到的结果是:
按速度:案例2==案例3>案例1

但是案例3不应该比案例2快很多吗。我的实验设计或对预期结果的假设是否有问题

编辑: 彼得·科尔德斯(Peter Cordes)指出了一些非常有用的文章,供进一步阅读:



您正确理解了
final
的影响(可能除了案例2的内部循环),但您的成本估算相差甚远。我们不应该期望在任何地方产生大的影响,因为mt19937的速度非常慢,而且所有3个版本的大部分时间都花在它上面


唯一没有在噪音/开销中丢失/隐藏的是将
int run\u once()覆盖final
内联到
FooPlus::run\u multiple
中的内部循环的效果,这种内联在案例2和案例3中都会运行。

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

案例2必须反复调用
run\u multiple
,但这只是
run\u one
的每100次运行一次,并且没有可测量的效果


对于所有3例患者,大部分时间花费在
dist(rng)上,因为与不内联函数调用的额外开销相比,
std::mt19937
非常慢。无序执行可能也会隐藏很多开销。但不是全部,所以还有一些东西需要衡量

案例3能够将所有内容内联到此asm循环(从quickbench链接):

案例2仍然可以内联
run\u once
run\u multiple
中,因为
class FooPlus
使用
int run\u once()覆盖final
。在外循环(仅)中存在虚拟调度开销,但每个外循环迭代的这一小部分额外成本与内循环的成本相比完全相形见绌(在案例2和案例3中相同)

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


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


如果您查看快速工作台连杆上的拆卸情况,则案例1和案例2的外环具有相同的asm

任何一个都不能进行设备化和内联
运行\u multiple
。案例1因为它是虚拟的非final,案例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
类型<代码>运算符new
可以被重写,但编译器仍会发出对
FooPlus::FooPlus()
的单独调用(从
new
向其传递指向存储的指针)。因此,这似乎是一个在案例2和案例1中没有利用的叮当声。

运行
100
迭代对我来说似乎不太多。另外,生成随机数的速度很慢,这很容易掩盖虚拟调度的速度,即使虚拟调度比静态调度慢,虚拟调度也应该非常快。尽管如此,这里是Quickbench链接,可调参数设置为1000 ITER。结果仍然是一样的:我有时做的是在开始测试之前生成所有随机数,然后从数组中一次提取一个(当我用完时环绕)。@Galik:你想要的词是“环绕”。您使用的另一个词有非常不同的含义(和发音)。:)很公平-让我试试。“但是案例1不能将Foo::run_once()内联到Foo::run_multiple(),因此与其他两种情况不同,内部循环中存在函数调用开销。”当内联虚拟函数调用可行时,哪些规则规定了内联规则?添加“final”限定符是一个强烈的提示吗?@tangy:编译器必须能够证明没有派生类型可以重写它。因此,它必须是
final
,或者在该调用站点的上下文中,它必须能够证明它有一个实际的
FooPlus
,并且肯定不是从它派生的可以有自己覆盖的东西。(或者一些编译器会在他们认为自己知道类型的情况下,通过创建一个内联循环(如果虚拟函数指针与它内联的独立定义匹配,则运行该内联循环),或者使用另一个正常使用vtable的asm块进行推测性的反虚拟化。)@Peter Cordes:" ... 编译器可以证明基f来自…,编译器无法证明