当前位置: 首页 > news >正文

跟我学C++中级篇——虚函数的性能

一、虚函数性能

一般来说,面向对象的设计中,继承和多态是其中两个非常重要的特征。从使用的过程来看,一般应用到继承的,使用多态的可能性就非常大。而多态的实现有很多种,
但开发者通常认为的多态(动多态)一般是指通过虚函数来实现的多态。
虚函数的不同于普通函数,它会通过一个虚表来控制函数的二次跳转或者叫做重定向。在普通的认知里,虚函数这个特征一般是无法进行诸如内联等进行优化的。所以一谈到虚函数都会认为其性能堪忧。
但在前面的分析中也知道了,什么东西都有特殊情况。但无论如何说明,在常识里,虚函数就是要比普通函数的性能要低。那么到底虚函数性能为什么会低?是不是所有情况下都低?下面进行一下分析。

二、用例子看问题

先看对比的例子:

#include <iostream>
#include <cmath>
#include <algorithm>
#include <iostream>
#include <chrono>
class Parent
{
public:virtual double exeReadDataV(double a, int b){return std::sqrt(a) * std::sin(b);}double exeReadData(double a, int b){return  std::sqrt(a) * std::sin(b);}
};int main()
{Parent* p = new Parent();double a = 3.14f;int b = 3;double sum = 0.f;auto t1 = std::chrono::steady_clock::now();for (int num = 0; num < 100000000; num++){//使用一个定值,一个动态值sum += p->exeReadDataV(a,num);//全是定值//sum += p->exeReadDataV(a, b);}auto t2 = std::chrono::steady_clock::now();auto escape1 = t2 - t1;std::cerr << "escape1 is:" << escape1.count() << std::endl;auto t3 = std::chrono::steady_clock::now();for (int num = 0; num < 100000000; num++){sum += p->exeReadData(a,num);//sum += p->exeReadData(a, b);}auto t4 = std::chrono::steady_clock::now();auto escape2 = t4 - t3;std::cerr << "escape2 is:" << escape2.count() << std::endl;return 0;
}

执行结果:

escape1 is:1982749665
escape2 is:1850111712

从上面的执行可以看到,两者的执行基本没区别。这可能打破了不少人的感官认知。无论哪种函数,决定性能的有两个重要环节:一个是调用的开销;另外一个是确定性调用。前者比较好理解,后者则不容易弄明白。其实可以这样理解,一个写代码要尽量降低调用的开销,一个是写出的代码编译器能更准确的知晓上下文,然后进行优化。在前者确定的情况下,后者就非常重要了。而虚函数被大多数人认为性能低的主要原因就在于后者。
虚函数需要一个虚表进行跳转,在内存中这种开销与函数的功能开销相对来说可以忽略。但这种跳转本身意味着大量的未知,而未知就意味着编译无法掌控更多的确定性,而对某些很简单的优化可能都无法进行。正如早期的编译器,在for循环中,直接给一个变量和一个表达式,效率差不少就是这个原因。而后的编译器则对此进行了优化,将其直接转成一个常量值。把上面的例子中注释部分打开并注释当前的执行(即两个参数都为定值的情况):

执行结果:

escape1 is:867604532
escape2 is:100431290

在执行的函数调用中,两个参数中一个为定值,一个为变量时,是否调用虚函数或者普通函数,基本运行是差不多的。但是一旦调用的都为定值时,此时普通函数可以直接将两个计算函数std::sqrt(a) 和 std::sin(b)均优化为固定值并只计算一次。此时再看,计算结果可就差了将一个量级了。
而在前面的文章中(“内联补遗”)分析过的虚函数可以内联,恰恰是那种可以明确确定的虚拟函数,可以内联。即编译器知道虚函数不具有多态性的情况下,它可普通函数没有什么区别。把原来的例子拿上来:

class A{
public:inline virtual void Test(){...}
};
class B:public A
{
public:inline  virtual void Test(){...}
};
inline void Get(A& a){a.Test();
}
int main(){A a;B b;b.Test();//可以内联//下面不确定Get(b);Get(a);
return 0;
}

那么可以从内联的角度来分析,虚函数为什么会给大家一个性能低的印象?首先为什么内联函数快,主要就是固定地址,编译器优化两大方面。而上面的虚拟函数可以内联,仿佛是与此结论相反,但恰恰提到了能够内联的虚拟函数的情形。互相印证,应该就明白为什么虚函数在多态的情况下性能低的原因了。
总之,虚函数本身不是性能低的代表,但是虚函数多态的调用会影响优化才是性能降低的根源。这些优化包括计算优化、分支跳转优化以及调用优化等等。而这些优化无法被编译器使用,自然也就会使得编译出来的代码有着很多的多余的运行指令。

三、设计上对虚函数数的替代

如果对性能的敏感性不强,那么如何使用虚函数不是一个多大的问题。可是如果实际情况对此要求比较严格时,可以考虑用如下的方式来解决虚函数的使用问题:
1、模板的方法如CRTP(奇异递归)
2、使用宏(不推荐)
3、通过设计模式等设计方法实现(如访问者模式等 )
4、使用一些技术或方法绕开多态,比如就直接写多个类然后直接控制
到底如何使用或不使用虚函数,是根据实际情况来决定的。还是那句话,没有一个技术是包打天下的。

四、总结

学习知识不是简单的为了会用,而是能够灵活的运用。要想灵活的运用,则必须掌握技术本质的内涵。只有把其内在体系掌握,才能在具体的场景上发挥其优势。这也是总说的从必然世界到自由世界的一个哲学问题。

http://www.lryc.cn/news/402932.html

相关文章:

  • trl - 微调、对齐大模型的全栈工具
  • GuLi商城-商品服务-API-品牌管理-品牌分类关联与级联更新
  • 【linux】服务器ubuntu安装cuda11.0、cuDNN教程,简单易懂,包教包会
  • 在 Apifox 中如何高效批量添加接口请求 Body 参数?
  • 专业PDF编辑工具:Acrobat Pro DC 2024.002.20933绿色版,提升你的工作效率!
  • 车载音视频App框架设计
  • StarRocks on AWS Graviton3,实现 50% 以上性价比提升
  • VUE中setup()
  • 【单元测试】SpringBoot
  • 分布式搜索引擎ES-elasticsearch入门
  • TCP三次握手与四次挥手详解
  • 【Windows】操作系统之任务管理器(第一篇)
  • 图同构的必要条件
  • Django获取request请求中的参数
  • kotlin compose 实现应用内多语言切换(不重新打开App)
  • 记录些MySQL题集(16)
  • 【算法基础】Dijkstra 算法
  • 使用 Flask 3 搭建问答平台(三):注册页面模板渲染
  • pycharm如何debug for循环里面的错误值
  • 解决网页中的 video 标签在移动端浏览器(如百度访问网页)视频脱离文档流播放问题
  • .Net--CLS,CTS,CLI,BCL,FCL
  • Stable Diffusion:质量高画风清新细节丰富的二次元大模型二次元插图
  • 数读MEME之争:以太坊获更高价值共识,抢占热点成Solana流量密码
  • python的with语句
  • Selenium原理深度解析
  • 算法复杂度<数据结构 C版>
  • 【XSS】
  • Go网络编程-RPC程序设计
  • Linux 性能优化:轻松入门
  • C++相关概念和易错语法(22)(final、纯虚函数、继承多态难点)