关于虚函数的成本

时间:2009-08-18 09:03:21

标签: c++ virtual

如果我在一个循环中调用虚拟函数1000次,我是否会遭受vtable查询开销1000次或仅一次?

7 个答案:

答案 0 :(得分:8)

编译器可能能够对其进行优化 - 例如,以下(至少在概念上)可以轻松优化:

Foo * f = new Foo;
for ( int i = 0; i < 1000; i++ ) {
   f->func();
}

但是,其他情况更难:

vector <Foo *> v;
// populate v with 1000 Foo (not derived) objects
for ( int i = 0; i < v.size(); i++ ) {
   v[i]->func();
}

同样的概念优化是适用的,但编译器要查看起来要困难得多。

底线 - 如果您真的关心它,请在启用所有优化的情况下编译代码并检查编译器的汇编程序输出。

答案 1 :(得分:6)

Visual C ++编译器(至少通过VS 2008)不会缓存vtable查找。更有趣的是,它不会直接调度对象的静态类型为sealed的虚方法。但是,虚拟调度查找的实际开销几乎总是可以忽略不计。您有时会看到命中的地方在于,C ++中的虚拟调用不能像托管VM中的直接调用那样被替换。这也意味着没有内联虚拟呼叫。

确定应用程序影响的唯一真正方法是使用分析器。

关于原始问题的具体细节:如果您调用的虚拟方法非常简单,虚拟调度本身会产生可测量的性能影响,那么该方法足够小,以至于vtable将保留在处理器的缓存中。环。即使从vtable中提取函数指针的汇编指令执行1000次,性能影响也会远远小于(1000 * time to load vtable from system memory)

答案 2 :(得分:3)

如果编译器可以推断出你正在调用虚函数的对象没有改变,那么,理论上,它应该能够将vtable查找从循环中提升出来

您的特定编译器是否实际执行此操作是您只能通过查看它生成的汇编代码才能找到的。

答案 3 :(得分:1)

我认为问题不是vtable查找,因为这是非常快速的操作,特别是在一个循环中,你在缓存上有所有必需的值(如果循环不是太复杂,但如果它很复杂,那么虚函数不会影响性能很多)。问题在于编译器无法在编译时内联该函数。

当虚函数非常小(例如只返回一个值)时,这尤其成问题。在这种情况下,相对性能影响可能很大,因为您需要函数调用才能返回一个值。如果可以内联此函数,则会极大地提高性能。

如果虚拟功能性能消耗,那么我真的不关心vtable。

答案 4 :(得分:1)

有关虚拟函数调用开销的研究,我推荐本文 “用C ++调用虚函数的直接成本”

答案 5 :(得分:1)

让我们尝试使用g ++定位x86:

$ cat y.cpp
struct A
  {
    virtual void not_used(int);
    virtual void f(int);
  };

void foo(A &a)
  {
    for (unsigned i = 0; i < 1000; ++i)
      a.f(13);
  }
$ 
$ gcc -S -O3  y.cpp  # assembler output, max optimization
$ 
$ cat y.s
    .file   "y.cpp"
    .section    .text.unlikely,"ax",@progbits
.LCOLDB0:
    .text
.LHOTB0:
    .p2align 4,,15
    .globl  _Z3fooR1A
    .type   _Z3fooR1A, @function
_Z3fooR1A:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    pushq   %rbx
    .cfi_def_cfa_offset 24
    .cfi_offset 3, -24
    movq    %rdi, %rbp
    movl    $1000, %ebx
    subq    $8, %rsp
    .cfi_def_cfa_offset 32
    .p2align 4,,10
    .p2align 3
.L2:
    movq    0(%rbp), %rax
    movl    $13, %esi
    movq    %rbp, %rdi
    call    *8(%rax)
    subl    $1, %ebx
    jne .L2
    addq    $8, %rsp
    .cfi_def_cfa_offset 24
    popq    %rbx
    .cfi_def_cfa_offset 16
    popq    %rbp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE0:
    .size   _Z3fooR1A, .-_Z3fooR1A
    .section    .text.unlikely
.LCOLDE0:
    .text
.LHOTE0:
    .ident  "GCC: (GNU) 5.3.1 20160406 (Red Hat 5.3.1-6)"
    .section    .note.GNU-stack,"",@progbits
$

L2标签是循环的顶部。 L2之后的线似乎正在将vpointer加载到rax中。 L2之后的4行调用似乎是间接的,从vstruct获取指向f()覆盖的指针。

我对此感到惊讶。我原以为编译器会将f()覆盖函数的地址视为循环不变量。似乎gcc正在做出两个“偏执”的假设:

  1. f()覆盖函数可能会更改对象中隐藏的vpointer 不知何故,还是
  2. f()覆盖功能可能会改变内容 vstruct不知怎的。
  3. 编辑:在一个单独的编译单元中,我实现了A :: f()和一个调用foo()的main函数。然后,我使用链接时优化构建了一个带有gcc的可执行文件,并在其上运行了objdump。内联虚函数调用。所以,也许这就是为什么没有LTO的gcc优化不像人们预期的那样理想。

答案 6 :(得分:0)

我想说这取决于你的编译器以及循环的外观。 优化编译器可以为您做很多事情,如果VF调用是可预测的,编译器可以帮助您。 也许你可以在编译器文档中找到关于编译器优化的一些内容。