【问题标题】:about the cost of virtual function关于虚函数的代价
【发布时间】:2010-11-20 12:47:50
【问题描述】:

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

【问题讨论】:

  • 比起查找本身所需的时间,我更关心缓存未命中和错误预测的跳转目标。这些问题在很大程度上取决于先前访问的精确模式。关于微观性能比较的标准反应是“测量”,现在是“测量并注意你测量的东西确实是部署的特征”。您还将依赖处理器的精确模型、其他内核正在运行什么、SMT 效果……

标签: c++ virtual


【解决方案1】:

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

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

    • 非常感谢您的回答和cmets。有时间我会在 gcc 上检查一下。
    【解决方案3】:

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

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

    【讨论】:

    • ... 或通过分析。编写一个必须进行 1000 次查找和比较的代码。
    • 但是你会将它与什么比较呢?您无法将其与非虚拟函数进行比较,因为它调用的是绝对地址而不是间接地址。您也无法将其与调用 1000 个不同对象的代码进行比较,因为 a)您必须从某个地方获取这些对象的地址,这需要额外的时间,并且 b)调用 1000 个不同的对象对缓存的友好性要低得多,所以我们无论如何都会希望它变慢。
    • 与 volatie Foo* 相比。请注意,您首先必须检查 1000 个非虚拟调用,以查看在每次调用时重新加载 this 会产生多少开销。然后,比较易失性和非易失性 Foo* 的 1000 多个虚拟调用,看看 vtable 查找产生了多少额外开销。
    【解决方案4】:

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

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

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

        让我们尝试一下 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 以某种方式。

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

        【讨论】:

          【解决方案7】:

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

          【讨论】:

          • 我知道它“可以”,如果它“确实”帮助我,我不知道。
          猜你喜欢
          • 2021-08-28
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-12
          • 1970-01-01
          • 1970-01-01
          • 2010-11-03
          相关资源
          最近更新 更多