静态调用虚函数时

When virtual functions are invoked statically?

本文关键字:函数 调用 静态      更新时间:2023-10-16

直接从派生类指针调用虚函数与从基类指针调用同一派生类之间的性能差异是什么?

在派生指针的情况下,调用是静态绑定还是动态绑定?我认为它将是动态绑定的,因为无法保证派生指针实际上不会指向进一步的派生类。如果我直接按值(而不是通过指针或引用)获得派生类,情况会改变吗?所以这3种情况:

  1. 指向派生的基本指针
  2. 派生指向派生的指针
  3. 按值派生

我担心性能,因为代码将在微控制器上运行。

演示代码

struct Base {
// virtual destructor left out for brevity
virtual void method() = 0;
};
struct Derived : public Base {
// implementation here
void method() {
}
}
// ... in source file
// call virtual method from base class pointer, guaranteed vtable lookup
Base* base = new Derived;
base->method();
// call virtual method from derived class pointer, any difference?
Derived* derived = new Derived;
derived->method();
// call virtual method from derived class value
Derived derivedValue;
derived.method();
  • 理论上,唯一能产生影响的C++语法是使用限定成员名称的成员函数调用。就您的类定义而言,这将是

    derived->Derived::method();
    

    此调用忽略对象的动态类型并直接转到Derived::method(),即它是静态绑定的。这仅适用于调用在类本身或其祖先类之一中声明的方法。

    其他一切都是常规的虚函数调用,根据调用中使用的对象的动态类型进行解析,即它是动态绑定的。

  • 在实践中,编译器将努力优化代码,并在编译时已知对象的动态类型的上下文中将动态绑定调用替换为静态绑定调用。例如

    Derived derivedValue;
    derivedValue.method();
    

    通常会在几乎每个现代编译器中生成静态绑定调用,即使语言规范没有为这种情况提供任何特殊处理。

    此外,直接从构造函数和析构函数进行的虚拟方法调用通常编译为静态绑定调用。

    当然,智能编译器可能能够在更多种类的上下文中静态绑定调用。例如,两者

    Base* base = new Derived;
    base->method();
    

    Derived* derived = new Derived;
    derived->method();
    

    编译器可以将其视为轻松允许静态绑定调用的琐碎情况。

必须编译虚拟函数才能像始终以虚拟方式调用一样工作。如果编译器将虚拟调用编译为静态调用,则优化必须满足此假设规则。

由此可见,编译器必须能够证明所讨论对象的确切类型。并且有一些有效的方法可以做到这一点:

  • 如果编译器看到对象的创建(从中获取地址的new表达式或自动变量),并且可以证明该创建实际上是当前指针值的源,则为其提供所需的精确动态类型。您的所有示例都属于此类别。

  • 当构造函数
  • 运行时,对象的类型恰好是包含正在运行的构造函数的类。因此,在构造函数中进行的任何虚函数调用都可以静态解析。

  • 同样,当析构函数
  • 运行时,对象的类型恰好是包含正在运行的析构函数的类。同样,任何虚拟函数调用都可以静态解析。

Afaik,这些都是允许编译器将动态调度转换为静态调用的情况。

所有这些都是优化,但是,编译器可能会决定执行运行时 vtable 查找。但是好的优化编译器应该能够检测到所有三种情况。

前两种情况之间应该没有区别,因为虚函数的思想是始终调用实际实现。撇开编译器优化(理论上可以优化所有虚函数调用,如果你在同一编译单元中构造对象,并且无法在两者之间更改指针),第二次调用也必须作为间接(虚拟)调用实现,因为可能有第三个类继承自 Derived 并实现该函数。我假设第三次调用不会是虚拟的,因为编译器在编译时就已经知道实际类型。实际上,您可以通过不将函数定义为虚拟来确保这一点,如果您知道您将始终直接调用派生类。

对于在小型微控制器上运行的真正轻量级代码,我建议完全避免将函数定义为虚拟函数。通常不需要运行时抽象。如果你编写了一个库并且需要某种抽象,你可以改用模板(这给了你一些编译时的抽象)。

至少在 PC CPU 上,我经常发现虚拟调用是您可能拥有的最昂贵的间接寻址之一(可能是因为分支预测更困难)。有时还可以将间接寻址转换为数据级别,例如,您保留一个对不同数据进行操作的泛型函数,这些数据通过指向实际实现的指针进行定向。当然,这仅在某些非常特殊的情况下有效。

在运行时。

但是:性能与什么相比?将虚拟函数调用与非虚拟函数调用进行比较是无效的。您需要将其与非虚函数调用加上ifswitch、间接寻址或其他提供相同函数的方法进行比较。如果函数没有体现实现中的选择,即不需要是虚拟的,请不要将其设为虚拟。