9

如果我在一个循环中调用一个虚函数 1000 次,我会遭受 1000 次或仅一次的 vtable 查找开销吗?

4

7 回答 7

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();
}

相同的概念优化是适用的,但编译器更难看到。

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

于 2009-08-18T09:07:13.697 回答
6

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

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

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

于 2009-08-18T09:10:48.823 回答
3

如果编译器可以推断出您调用虚函数的对象没有改变,那么理论上它应该能够将 vtable 查找提升到循环之外。

您的特定编译器是否真的做到了这一点,您只能通过查看它生成的汇编代码来确定。

于 2009-08-18T09:06:37.550 回答
1

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

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

如果虚函数消耗性能,那么我就不会真正关心 vtable。

于 2009-08-18T09:37:28.783 回答
1

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

于 2013-10-23T20:09:10.553 回答
1

让我们尝试一下针对 x86 的 g++:

$ 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 的内容。

编辑:在一个单独的编译单元中,我实现了 A::f() 和一个调用 foo() 的主函数。然后,我使用链接时优化使用 gcc 构建了一个可执行文件,并在其上运行了 objdump。虚函数调用是内联的。所以,也许这就是为什么没有 LTO 的 gcc 优化不像人们预期的那样理想。

于 2016-10-25T21:45:58.413 回答
0

我会说这取决于您的编译器以及循环的外观。优化编译器可以为您做很多事情,如果 VF 调用是可预测的,那么编译器可以为您提供帮助。也许您可以在编译器文档中找到有关编译器所做优化的一些信息。

于 2009-08-18T09:12:03.687 回答