专业编程基础技术教程

网站首页 > 基础教程 正文

C++虚函数会导致性能开销大? c++虚函数使用

ccvgpt 2024-11-12 09:55:11 基础教程 9 ℃

C++虚函数作用

C++中的虚函数的作用主要是实现了多态的机制。

虚函数是通过一张虚函数表(Virtual Table)实现的。在这个表中,主要是一个类的虚函数地址表,这张表解决了继承和覆盖等问题。就像一张地图一样,指明了指针实际所应该调用的函数。

C++虚函数会导致性能开销大? c++虚函数使用


C++虚函数特点

①我们在声明了一个有虚函数的类的对象同时,这个对象被添加了一个隐式成员。该成员保存了指向虚函数地址数组的指针。

②只要包含虚函数的类就会有一个虚函数表,当这个类是基类时,它的派生类也会有相应的虚函数表。当一个类有多个对象的时候,这些对象共享一个虚函数表。

虚函数表是编译器生成的,程序运行时被载入内存。

示意代码:

#include <iostream>
using namespace std;
class A
{
public:
    int i;
    virtual void func() {}
    virtual void func2() {}
};
class B : public A
{
    int j;
    void func() {}
};
int main()
{
    A a;
    B b;
    size_t len_a = sizeof(a);
    size_t len_b = sizeof(b);
    cout << len_a << ", " << len_b<<endl;  
    return 0;
}

示意图:


简单总结:

虚函数是动态多态(程序运行时多态,重载为静态多态)。实现方式为:

1、当类自身有虚函数,则会创建一张虚函数表,放置于类的开头,32bit的编译器下,虚函数表的指针大小是4字节。64bit的编译器下,虚函数表的指针大小是8字节。

2、当类的父类中有虚函数,则派生类会继承父类的虚函数表,同时若派生类有新的虚函数,则会在父类的虚函数表后面追加。

3、当派生类为多重继承,并且多个父类都有虚函数,则会全部继承父类的虚函数表,顺序以继承顺序,并且派生类中新增的虚函数只会添加在第一个父类虚函数表中。

4、当派生类重写父类的虚函数,则重写后的函数会取代原来虚函数在虚函数表中的位置。

虚函数的函数调用机制

①正常的函数调用:

  1. 复制栈上的一些寄存器,以允许被调用的函数使用这些寄存器;
  2. 将参数复制到预定义的位置,这样被调用的函数可以找到对应参数;
  3. 入栈返回地址;
  4. 跳转到函数的代码,这是一个编译地址,因为编译器/链接器硬编码为二进制;
  5. 从预定义的位置获取返回值,并恢复想要使用的寄存器。

②虚函数调用:

虚函数调用与此完全相同,唯一的区别就是编译时不知道函数的地址,而是,

  1. 从对象中获取虚表指针,该指针指向一个函数指针数组,每个指针对应一个虚函数;
  2. 从虚表中获取正确的函数地址,放到寄存器中;
  3. 跳转到该寄存器中的地址,而不是跳转到一个硬编码的地址。


虚函数可能会造成性能损失的原因总结

从上面的分析可以看出来,虚函数可能会造成性能损失:

● 构造函数必须初始化vptr。

● 虚函数是通过指针间接调用的,所以必须先得到指向虚函数表的指针,然后再获得正确的函数偏移量。

● 内联是在编译时决定的。编译器不可能把运行时才解析的虚函数设置为内联。

客观地看待C++,前两项并不能算是性能损失。无法内联虚函数造成的性能损失最大

总结

虚函数其实最主要的性能开销在于它阻碍了编译器内联函数和各种函数级别的优化,导致性能开销较大。如果代码中使用了更多的虚函数,编译器能优化的代码就越少,性能就越低。所以,评估虚函数的性能损失就是评估无法内联该函数所造成的损失。这种损失的代价并不固定,它取决于函数的复杂程度和调用频率。

虚函数通常通过虚函数表来实现,在虚表中存储函数指针,实际调用时需要间接访问,这需要多一点时间。然而这并不是虚函数速度慢的主要原因,真正原因是编译器在编译时通常并不知道它将要调用哪个函数,所以它不能被内联优化和其它很多优化,因此就会增加很多无意义的指令(准备寄存器、调用函数、保存状态等)。真正的问题不是虚函数,而是那些不必要的间接调用。

通常,使用虚函数没问题,它的性能开销也不大,而且虚函数在面向对象代码中有强大的作用。但是不能无脑使用虚函数,特别是在性能至关重要的或者底层代码中,而且大项目中使用多态也会导致继承层次很混乱。


有什么好方法替代虚函数呢?

  • 使用访问者模式来使类层次结构可扩展;
  • 使用普通模板替代继承和虚函数;

为了消除虚函数调用,必须允许编译器在编译期间就解析函数的绑定。通过对类选择进行硬编码或者将它作为模板参数来传递,可以避免使用动态绑定。

  • 使用variants替代虚函数或模板方法。

最近发表
标签列表