【问题标题】:Optimization of virtual function calls in derived class派生类中虚函数调用的优化
【发布时间】:2014-05-15 18:34:31
【问题描述】:

在派生类中调用虚函数以便编译器可以内联或以其他方式优化调用的最佳方法是什么?

例子:

class Base {
  virtual void foo() = 0;
};

class Derived: public Base {
  virtual void foo() {...}
  void bar() {
    foo();
  }
};

我希望在bar() 中对foo() 的调用始终调用Derived::foo()。 据我了解,调用将导致 vtable 查找,编译器无法优化它,因为可能有另一个类继承自 Derived。

我可以显式调用Derived::foo(),但如果Derived 中有许多虚函数调用,那会变得冗长。我还发现令人惊讶的是,我在网上找不到很多材料来解决我认为常见的情况(调用虚方法的“最终”派生类),所以我想知道我是否在这里滥用虚函数或过度优化。

这应该怎么做?停止过早优化并坚持使用foo(),吸干它并使用Derived::foo(),还是有更好的方法?

【问题讨论】:

  • 我认为你的最后一行总结得很好。
  • 如果你知道什么都不会覆盖foo(),那么声明它final。见:stackoverflow.com/questions/8824587/…
  • 你有没有测量过编译器是否真的已经为你优化了?
  • 哦,太棒了。 final 正是我想要的。我不知道它存在于 C++ 中。
  • 我不认为 final 给你更多的优化可能性。编译器仍然必须确定层次结构中是否有另一个类覆盖 foo,它只是说层次结构中没有类。如果编译器可以对 final 方法执行此操作并对其进行去虚拟化,那么在没有 final 的情况下也应该可以。

标签: c++ inheritance optimization virtual inline


【解决方案1】:

C++11 包含 final 关键字,它“指定在派生类中不能重写虚函数或不能继承类。”1

如果已声明final,g++ 似乎能够优化派生类中的虚函数调用。

我创建了以下测试:

virtualFunctions.h

#pragma once
class Base {
public:
  virtual void foo();
  virtual void bar();
  virtual void baz();
  int fooVar, barVar, bazVar;
};
class Derived: public Base {
public:
  void test();
  virtual void foo();
  virtual void bar();
  virtual void baz() final;
};

virtualFunctions.cpp:

#include "virtualFunctions.h"
void Derived::test() {
  foo();
  Derived::bar();
  baz();
}
void Derived::foo() {
  fooVar = 101;
}
void Derived::bar() {
  barVar = 202;
}
void Derived::baz() {
  bazVar = 303;
}

我正在使用 g++ 4.7.2 和 -O1 生成的程序集包含:

_ZN7Derived4testEv:
.LFB0:
    .loc 1 3 0
    .cfi_startproc
.LVL3:
    pushl   %ebx
.LCFI0:
    .cfi_def_cfa_offset 8
    .cfi_offset 3, -8
    subl    $24, %esp
.LCFI1:
    .cfi_def_cfa_offset 32
    movl    32(%esp), %ebx      ; Load vtable from the stack
    .loc 1 4 0
    movl    (%ebx), %eax        ; Load function pointer from vtable
    movl    %ebx, (%esp)
    call    *(%eax)             ; Call the function pointer
.LVL4:
    .loc 1 5 0
    movl    %ebx, (%esp)
    call    _ZN7Derived3barEv   ; Direct call to Derived::bar()
.LVL5:
    .loc 1 6 0
    movl    %ebx, (%esp)
    call    _ZN7Derived3bazEv   ; Devirtualized call to Derived::baz()

Derived::bar()Derived::baz()都是直接调用的,而vtable用于foo()

【讨论】:

    【解决方案2】:

    如果编译器可以静态找出使用的类型,则编译器可能能够对其进行优化并执行去虚拟化。

    虚拟方法调用非常便宜。前段时间我读到一篇文章,说与普通方法调用相比,开销大约是百分之十。这当然不考虑丢失的内联机会。

    我也觉得这混合了接口和实现。我觉得还是拆分成纯接口和实现类比较好。

    【讨论】:

      【解决方案3】:

      正如您自己所说,只有在极少数情况下,您才应该担心这种情况对性能的影响。 如果您正在编译为 C++11,您可以将 Derived 和/或 foo()/bar() 声明为 final,编译器可能将其内联。

      【讨论】:

        【解决方案4】:

        问题的答案是禁用动态调度,这可以通过限定来完成:

        class Derived: public Base {
          virtual void foo() {...}
          void bar() {
            Derived::foo();          // no dynamic dispatch
          }
        };
        

        现在的问题是这是否会对性能产生影响(在改变之前进行测量!)以及这样做是否有意义。虚函数是派生类型的扩展点。如果您禁用动态调度,有人可能会创建MoreDerived,实现foo 并期望bar 调用MoreDerived::foo,但如果您禁用动态调度,则不会发生。

        除非有一个非常好的、可衡量的、尝试微优化的理由,否则完全避免这个问题。如果您在分析器中运行代码,动态调度可能根本不会出现。

        【讨论】:

        • 动态和静态分派之间的差异不大,但动态和 no 分派(即内联)之间的差异可能很大。例如像素访问。
        • @mark:有些情况很重要,有些情况不重要。后者比前者更常见,如果没有测量,我不会这样做。这就是答案,用动态调度测量和不测量,比较。另请注意,还有其他选择,该功能是否需要是虚拟的?是否有一个 impl 函数不是从两个上下文中调用的?禁用对函数的动态调度很容易引起意外。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-09-08
        • 1970-01-01
        • 1970-01-01
        • 2020-01-04
        • 1970-01-01
        • 2015-10-24
        • 2011-09-27
        相关资源
        最近更新 更多