派生类中虚函数调用的优化

Optimization of virtual function calls in derived class

本文关键字:优化 函数调用 派生      更新时间:2023-10-16

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

的例子:

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

我希望bar()中对foo()的调用总是调用Derived::foo()。这是我的理解,调用将导致虚函数表查找和编译器无法优化它,因为可能有另一个类继承从派生。

我可以显式地调用Derived::foo(),但如果在派生中有许多虚拟函数调用,则会变得冗长。我也发现令人惊讶的是,我在网上找不到太多的材料来解决对我来说似乎是一个常见的情况(调用虚拟方法的"最终"派生类),所以我想知道我是否在这里滥用虚拟函数或过度优化。

这应该怎么做?停止过早地优化并坚持使用foo(),接受它并使用Derived::foo(),或者有更好的方法吗?

c++ 11包含final关键字,该关键字"指定虚函数不能在派生类中被重写,或者不能从该类继承。"

如果在派生类中声明了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,使用- 01生成的程序集包含:

_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()

如果编译器可以静态地找出所使用的类型,它可能能够优化它并执行反虚拟化。

虚拟方法调用非常便宜。不久前,我读到一篇文章,指出与普通方法调用相比,开销大约是10%。这当然没有考虑到内联机会的缺失。

我也有一种感觉,这混合了接口和实现。我认为最好把它分成一个纯接口和一个实现类。

正如您自己所说,只有在极端罕见的情况下,您才应该考虑这样做对性能的影响。如果你用c++ 11编译,你可以声明Derived和/或foo()/bar()为final,编译器可能会内联它。

这个问题的答案是禁用动态分派,这可以通过限定来实现:

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

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

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