C++代码优化(五):虚函数的开销和优化方式

目录

1.虚函数的工作原理

1.1.虚函数表(vtable)的结构和作用

1.2.虚函数调用的内部过程

2.虚函数的开销

3.如何减少虚函数调用开销

3.1.使用final和override关键字

3.2.非虚接口(NVI)模式

3.3.使用 CRTP(Curiously Recurring Template Pattern)替代虚函数

3.4.使用函数指针替代虚函数

3.5.使用内联函数

3.6.考虑使用其他设计模式

3.7.微软的下一代多态库

4.总结


1.虚函数的工作原理

        C++ 中,虚函数提供了动态多态性,让程序能够在运行时通过基类指针或引用调用派生类的重载函数。尽管虚函数在设计上灵活,但它也带来了额外的性能开销。

1.1.虚函数表(vtable)的结构和作用

        虚函数表(vtable),是C++中实现多态的关键数据结构。每个多态类都拥有一个vtable指针,通常隐藏在对象内存布局的最开始处。vtable包含了类中所有虚函数的指针,当通过基类指针或引用调用派生类中的虚函数时,程序会查看vtable来确定应调用哪个函数。

        虚函数表的结构对程序的性能有重要影响。在对象实例化时,vtable会被创建,并在运行时通过它来解析虚函数调用。这涉及额外的内存访问,可能会带来缓存不命中的问题,特别是在大规模对象系统中。然而,使用vtable的好处在于,它允许在不改变代码编译的情况下动态地改变程序的行为,这为C++的多态特性提供了基础。

        虚函数表的结构

        虚函数表是一个数组,每个元素都是一个指向虚函数的指针。以下是一个简单的虚函数表结构示例:

class Base {
public:virtual void func1() { /* implementation */ }virtual void func2() { /* implementation */ }virtual ~Base() { /* destructor */ }
};class Derived : public Base {
public:void func1() override { /* overridden implementation */ }void func2() override { /* overridden implementation */ }void func3() { /* new function */ }
};

对于上述类结构,编译器可能生成如下的虚函数表:

  • Base类的vtable:
  • Derived类的vtable:

注意:

  1. 虚函数表中的每个条目都是指向虚函数的指针。
  2. 虚析构函数也是虚函数表中的一部分,确保在删除派生类对象时能够正确调用析构函数链。
  3. 如果一个类没有显式声明虚函数,但它从有虚函数的基类继承,它仍然会有虚函数表。

1.2.虚函数调用的内部过程

当一个虚函数被调用时,程序会执行以下步骤:

  1. 查看对象的虚函数表指针。
  2. 通过虚函数表找到要调用函数的实际地址。
  3. 跳转到该地址执行函数代码。

这个过程比普通函数调用多了一层间接性,因此会有一定的性能开销。特别是在频繁的虚函数调用场景下,这些开销可能会累积成为显著的性能瓶颈。理解这一过程可以帮助开发者在设计和实现类层次结构时,更加明智地使用多态特性,优化系统性能。

2.虚函数的开销

  1. 虚函数表 (vtable) 开销:每个包含虚函数的类通常会生成一个虚函数表,用于存储指向虚函数实现的指针。每个多态对象在内存中包含一个指向虚表的指针,这个指针称为虚表指针 (vptr)。因此,每个对象会多占用一个指针大小的内存(通常为 4 或 8 字节,具体取决于系统架构)。

  2. 间接调用开销:调用虚函数需要先访问虚表,通过虚表指针找到具体函数的地址,然后进行调用。这种间接寻址比普通函数调用更慢,尤其在频繁调用虚函数的场景中,开销会显著增加。

  3. 内存布局影响:对象内的虚表指针增加了对象大小,并且在某些情况下会导致内存布局的不连续性,增加了 CPU 缓存未命中的概率,降低缓存命中率。

  4. 编译器优化受限:编译器对虚函数的优化受限。例如,普通的非虚函数调用可以内联 (inline) 优化,但虚函数由于其动态特性无法在编译期确定实际的调用目标,因此无法轻易内联。

        为了了解虚函数调用开销的实际大小,我们可以进行一些简单的性能测试。下面是一个示例程序,用于比较直接调用函数和通过虚函数指针调用函数的性能:

class Base {public:virtual void func() {// 虚函数的实现}
};class Derived : public Base {public:void func() override {// 派生类中虚函数的实现}
};void directCall(Base& obj) {obj.func();
}void indirectCall(Base* obj) {obj->func();
}int main() {Base b;Derived d;clock_t start, end;double directTime, indirectTime;start = clock();for (int i = 0; i < 10000000; i++) {directCall(b);}end = clock();directTime = (double)(end - start) / CLOCKS_PER_SEC;start = clock();for (int i = 0; i < 10000000; i++) {indirectCall(&b);}end = clock();indirectTime = (double)(end - start) / CLOCKS_PER_SEC;std::cout << "直接调用函数的时间:" << directTime << " 秒" << std::endl;std::cout << "通过虚函数指针调用函数的时间:" << indirectTime << " 秒" << std::endl;return 0;
}

        在这个程序中,我们定义了一个基类 Base 和一个派生类 Derived ,它们都有一个虚函数 func 。我们还定义了两个函数 directCall 和 indirectCall ,分别用于直接调用函数和通过虚函数指针调用函数。在 main 函数中,我们分别测量了直接调用函数和通过虚函数指针调用函数的时间,并输出结果。

        通过运行这个程序,我们可以得到直接调用函数和通过虚函数指针调用函数的时间。在我的测试环境中,直接调用函数的时间大约为 0.05 秒,而通过虚函数指针调用函数的时间大约为 0.15 秒。这表明,虚函数的调用开销大约是直接调用函数的三倍。

        需要注意的是,这个测试结果只是一个示例,实际的虚函数调用开销会受到很多因素的影响,如硬件平台、编译器优化、虚函数的实现复杂度等。因此,在实际应用中,我们需要根据具体情况进行性能测试,以确定虚函数的调用开销是否可以接受。

3.如何减少虚函数调用开销

3.1.使用final和override关键字

        在C++11及以后的版本中,可以使用final关键字来防止类被继承或虚函数被重写。这有助于编译器进行更彻底的优化,因为它消除了动态绑定的需要。
        override关键字用于显式指出派生类中的函数将覆盖基类中的虚函数。这不仅提高了代码的可读性,还允许编译器在编译时进行类型检查,从而减少潜在的错误。

        示例:

class Base {
public:virtual void process() { /* 虚函数 */ }
};class Derived final : public Base {
public:void process() final override { /* 最终实现,不允许进一步派生 */ }
};

在此例中,标记 Derived 为 final 类,编译器可以针对 process 做更多的优化,因为它明确知道没有进一步的派生类会重写 process注意:即便使用了 final,虚表仍然存在,但编译器可以在具体类型调用时优化掉虚函数的间接调用。

3.2.非虚接口(NVI)模式

        非虚接口模式是一种设计模式,它将虚函数的使用限制在基类中,并通过非虚成员函数来调用这些虚函数。这样,派生类只需要重写基类中的少数几个虚函数,而大部分逻辑则保持在基类的非虚成员函数中。
        这种模式有助于减少虚函数的调用次数,因为大部分工作都在基类的非虚成员函数中完成,而派生类只需要提供必要的重写。

3.3.使用 CRTP(Curiously Recurring Template Pattern)替代虚函数

C++惯用法之CRTP(奇异递归模板模式)_c++ crtp-CSDN博客

对于需要静态多态的情况,CRTP 是一种可以替代虚函数的高效方法。CRTP 利用模板参数在编译时实现静态多态性,避免了运行时的虚表查找。示例如下:

template <typename Derived>
class Base {
public:void process() {static_cast<Derived*>(this)->process();}
};class Derived : public Base<Derived> {
public:void process() { /* 具体实现 */ }
};

在此例中,Base 中的 process 是一个非虚函数,通过模板参数在编译期静态分发到 Derived::process。CRTP 适用于不需要动态多态的场景,可以有效减少虚函数带来的开销。

3.4.使用函数指针替代虚函数

        当只需动态绑定单个方法的调用时,可以考虑使用函数指针代替虚函数。这样既能提供动态多态性,又能减少虚表带来的额外内存开销。

        示例如下:

class Base {
public:using FuncPtr = void(*)(Base*);Base(FuncPtr func) : func_(func) {}void process() { func_(this); }private:FuncPtr func_;
};void processImplementation(Base* obj) {// 实现逻辑
}int main() {Base obj(processImplementation);  // 使用函数指针调用obj.process();return 0;
}

在这种情况下,通过构造函数传入的函数指针实现了类似虚函数的效果,但不需要虚表,从而减少了内存和运行时开销。

3.5.使用内联函数

        如果虚函数的实现非常简单,可以将其声明为内联函数。内联函数可以在编译时将函数体插入到调用点,避免了函数调用的开销。但是,内联函数也有一些限制,如函数体不能太大,否则会导致代码膨胀。

3.6.考虑使用其他设计模式

        在某些情况下,可以考虑使用其他设计模式来替代虚函数的使用。例如,使用策略模式、访问者模式或状态模式等,这些模式可以在不牺牲性能的情况下实现类似的多态性。但是,这些技术也有一些复杂性,需要根据具体情况进行选择。

3.7.微软的下一代多态库

C++20到来了,更好的符合零成本抽象的多态实现方式来了——微软Proxy库。

Proxy说是一个库,其实就只是一个头文件而已。

那么基于微软的的Proxy,应该如何编写上面的多态呢?

看下面的代码,特别是开头的两个struct类。 没错,C++里面struct也是类。

#include <iostream>
#include "proxy.h"// 声明一个代理类,最终会通过这个代理类去调用真正的类对象的成员函数
struct Show : pro::dispatch<void()>
{template <class T>void operator()(T& self) { self.show(); }
};
struct Model : pro::facade<Show> {};class Who {
public: void show() {std::cout << "model run!\n";}
};class Boy {
public:void show() {std::cout << "boy run!\n";}
};class Girl {
public:void show() {std::cout << "girl run!\n";}
};class Man {
public:void show() {std::cout << "man run!\n";}
};void justrun(pro::proxy<Model> m) { m.invoke<Show>();
}int main() {Who who;Girl girl;Boy boy;Man man;justrun(&who);justrun(&girl);justrun(&boy);justrun(&man);
}

        重点是开头的Show和Model两个类。 按照这样的方式,定义这样子的两个类,就描述了一个类的基本行为。

        之后你只需要用proxy调用这个行为就可以了。 用proxy就完美的用零成本抽象的方式极致的解决了C++的多态问题。

        那么,很多人会说,我哪里记得住开头两个类的定义方法啊,定义方式长得那么猥琐。

        别急,微软还贴心的用宏定义了简化定义方式:

#include<iostream>
#include "proxy.h"DEFINE_MEMBER_DISPATCH(Show, show, void());
DEFINE_FACADE(Model, Show);class Who {
public: void show() {std::cout << "model run!\n";}
};class Boy {
public:void show() {std::cout << "boy run!\n";}
};class Girl {
public:void show() {std::cout << "girl run!\n";}
};class Man {
public:void show() {std::cout << "man run!\n";}
};void justrun(pro::proxy<Model> m) { m.invoke<Show>();
}int main() {Who who;Girl girl;Boy boy;Man man;justrun(&who);justrun(&girl);justrun(&boy);justrun(&man);
}

现在,整个代码就变得清爽宜人了。

可见,在C++20时代了,C++长期被人诟病的性能损失问题:虚函数实现多态,这一终极短板,也终于被克服了。

4.总结

虚函数在提供动态多态性的同时,也引入了内存和性能开销。在高性能场景中,可以通过以下优化策略有效减少虚函数的开销:

  1. 使用 final 关键字优化派生链:避免进一步派生,允许编译器内联优化。

  2. 使用 CRTP 替代虚函数:在需要静态多态的场景中,CRTP 是高效的替代方案。

  3. 使用函数指针实现动态调用:可在不引入虚表的情况下实现动态多态。

通过这些优化方法,C++ 程序可以在保持多态灵活性的同时,最大化减少虚函数带来的性能开销。你在编程中,为了极致的性能,会用哪些方式来避免避免虚函数的开销?欢迎留言讨论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/19306.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

STM32单片机锁死

自己画了一块stm32f407板子&#xff0c;外部晶振用了25MHz&#xff0c;烧写了8MHz的程序&#xff0c;第一次烧写成功&#xff0c;第二次开始识别不到芯片&#xff0c;第一次烧写成功由于外部晶振为25Hz&#xff0c;芯片内频率计算器却是按照8MHz写的&#xff0c;所以得出最后的…

Windows文件资源管理器增强工具

引言&#xff1a; 资源管理器在我们使用电脑时是经常用到的&#xff0c;各种文件资源等的分类整理都离不开它。但是Windows Explorer确实不好用&#xff0c;不智能&#xff0c;不符合人体工程学。特别是在一些场合&#xff0c;在打开的一堆文件夹里&#xff0c;想从中找到自己要…

聚类中3个解空间的描述

深度学习中做分类任务时&#xff0c;我们常常根据最后的全连接层得到一组向量A&#xff08;比如&#xff1a;[0.9, 0.7, 0.2]&#xff09;&#xff0c;这组向量经过归一化得到向量B(比如&#xff1a;[0.5&#xff0c; 0.3&#xff0c; 0.2])&#xff0c;再根据B向量采用概率最大…

Empirical analysis of hardware-assisted GPU virtualization

​ 年份&#xff1a;2019 作者&#xff1a;Anshuj Garg 会议&#xff1a;ESCI 出版商&#xff1a;IEEE 摘要 本篇文章对vGPU虚拟化的性能开销、调度算法的影响、同构与异构工作负载的干扰效应&#xff0c;以及PCI直通与vGPU的性能差异进行了研究。结果表明&#xff0c;vGP…

Java面试题2024-Java基础

Java基础 1、 Java语言有哪些特点 1、简单易学、有丰富的类库 2、面向对象&#xff08;Java最重要的特性&#xff0c;让程序耦合度更低&#xff0c;内聚性更高&#xff09; 3、与平台无关性&#xff08;JVM是Java跨平台使用的根本&#xff09; 4、可靠安全 5、支持多线程 2、…

【案例分享】运用 Infragistics Ultimate UI 让工业物联网 IIoT 数据流更易于访问

客户概况 贝克休斯旗下的 Bently Nevada 是状态监测和资产保护领域的全球领导者。该公司拥有 60 多年的专业知识&#xff0c;在全球安装了超过 600 万个传感器和 100,000 个机架监测系统。 如今&#xff0c;Bently Nevada的开发团队正在使用现代 UI 工具包来增强他们的系统&a…

PHM技术:基于支持向量机的智能故障诊断 | 行星齿轮箱智能故障诊断

目录 1.数据获取 2.特征提取与选择 3.健康状态识别 1.数据获取 用的行星齿轮箱数据采集自图1中的多级齿轮传动系统实验台中&#xff0c;在实验过程中&#xff0c;分别模拟了8种行星齿轮箱的健康状态&#xff0c;包括正常、第一级太阳轮点蚀、第一级太阳轮齿根裂纹、第一级…

推荐一款Windows系统精简工具:NTLite

NTLite是一款可以对Windows系统优化的安装工具&#xff0c;使用这款完全中文的NTLite授权注册版让你不会因为注册或者语言导致无法正常的使用&#xff0c;如果你正需要马上下载使用吧。 NTLite基本简介 NTLite 中文版可以用来做什么&#xff0c;它其实是一款 Windows 系统精简…

ESP-IDF VScode 项目构建/增加组件 新手友好!!!

项目构建 1.新建文件夹&#xff0c;同时在该文件夹内新建.c和.h文件 如图所示&#xff0c;在components中新建ADC_User.c、ADC_User.h、CMakeLists.txt文件。当然这里你也可以不在components文件夹内新建文件&#xff0c;下面会说没有在components文件夹内新建文件构建项目的方…

Node Exporter 可观测性最佳实践

Node Exporter 介绍 Node Exporter 是一个开源的 Prometheus 指标收集器&#xff0c;它提供了大量关于宿主机系统的关键指标&#xff0c;如 CPU、内存、磁盘和网络使用情况。在 Kubernetes 环境中&#xff0c;Node Exporter 对于监控集群节点的健康状况至关重要。本文将介绍如…

Spring Boot汽车资讯:科技与速度的交响

3系统分析 3.1可行性分析 通过对本汽车资讯网站实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本汽车资讯网站采用SSM框架&#xff0c;JAVA作为开发语言&#…

外卖跑腿小程序源码如何满足多样需求?

外卖跑腿平台已经成了当代年轻人的便捷之选&#xff0c;校园中也不例外&#xff0c;那么外卖、跑腿小程序就需要满足用户多样化的需求&#xff0c;而这背后的源码扮演者最重要的角色。 用户类型的多样性 1.对上班族而言&#xff0c;他们希望外卖小程序能够快速下单、准确配送…

GeeRPC第一天 服务端与消息编码(1)

RPC 1. 系统架构图解释&#xff08;Graph&#xff09; 架构层次 RPC框架核心功能&#xff1a;这是系统的最上层&#xff0c;涵盖了框架的主要功能模块&#xff0c;直接与底层服务和用户交互。 服务层&#xff1a;主要负责服务的注册、发现和治理。 服务注册&#xff1a;将服务…

如何在谷歌浏览器中开启离线模式

在数字化时代&#xff0c;互联网已经成为我们生活中不可或缺的一部分。然而&#xff0c;有时候我们可能会遇到没有网络连接的情况&#xff0c;这时谷歌浏览器的离线模式就显得尤为重要。本教程将详细介绍如何在谷歌浏览器中轻松开启离线模式&#xff0c;并附带一些相关教程指南…

【进阶系列】正则表达式 #匹配

正则表达式 正则表达式是一个特殊的字符序列&#xff0c;它能帮助你方便的检查一个字符串是否与某种模式匹配。re模块使 Python 语言拥有全部的正则表达式功能。 一个正则表达式的匹配工具&#xff1a;regex101: build, test, and debug regex s "C:\\a\\b\\c" pri…

C++使用Alglib数学库进行非线性最小二乘拟合

目录 一、前言 二、主要函数分析 2.1 lsfitcreatef 2.2 lsfitsetcond 2.3 lsfitfit 2.4 lsfitresults 三、基础代码实现 3.1 定义待拟合函数 3.2 数据拟合 四、可视化代码实现 4.1 拟合h文件 4.2 拟合cpp文件 4.2 代码实验 一、前言 本文记录基于Alglib进行非线性…

Spring Boot汽车世界:资讯与技术的交汇

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

算法--“找零方案”问题

def main():d [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0] # 存储各种硬币的面值d_num [] # 存储每种硬币的数量total_money 0 # 收银员拥有的总金额# 输入每种硬币的数量temp input(请输入每种零钱的数量&#xff08;以空格分隔&#xff09;:)d_num0 temp.split() # 以空…

【UGUI】Unity 背包系统实现02:道具信息提示与显示

在游戏开发中&#xff0c;背包系统是一个常见的功能模块&#xff0c;用于管理玩家拾取的物品。本文将详细介绍如何在 Unity 中实现一个简单的背包系统&#xff0c;包括道具信息的提示和显示功能。我们将通过代码和场景搭建来逐步实现这一功能。 1. 功能需求清单 在实现背包系…

java基础概念31:常见API-Runtime

一、Runtime类常用方法 Runtime&#xff1a;表示当前虚拟机JVM的运行环境&#xff0c;只能有一个。 【注意】&#xff1a; 获取Runtime对象&#xff0c;只能通过getRuntime静态方法。 好处&#xff1a;不管在哪个类中调用Runtime方法&#xff0c;获取的都是同一个对象。即&…