虚函数的开销


转自

虚函数

C++标准没有强制规定虚函数如何实现,但大多数编译器以下列方式实现。在编译阶段,对于每种类型,编译器知道哪些函数是虚函数,也知道虚函数的地址。对于每种类型,它创建一个虚函数表,虚函数表持有指向虚函数地址的指针。

在这里插入图片描述
在上图的例子中,Instrument拥有三个虚函数:playwhatadjust。这里有四个继承自Instrument的子类,并且它们都实现了三个虚函数,每个类型都有自己的虚表。

在运行时,当对象第一次被创建时,编译器将会创建一个指向虚表的虚表指针。当程序创建了Brass类型的对象时,虚表指针vptr被创建来指向Brass虚函数表。每个实例都有虚表指针来指向虚函数表,但是所有相同类型的实例共享一个虚函数表

在这里插入图片描述

为了调用虚函数,运行时必须知道虚函数的地址。虽然程序不知道虚函数的地址,但是它知道虚函数表中的偏移量,其中虚函数表持有虚函数的所在的地址。在上图中,运行时系统知道虚函数play的地址在虚函数表的偏移是0;虚函数what的偏移是1。运行时系统访问vptr虚表指针来知道虚函数表的地址,通过偏移量找到希望调用的函数,然后得到函数地址,再然后执行虚函数的调用

虚函数表存放的位置

虚函数表是编译器在编译时构造的,是一个函数指针数组。该函数指针实际上是指向特定类的虚函数指针。更准确的说,虚函数表是一个函数指针的静态数组,因此同一个类的对象可以共享该虚函数表,因此,虚函数表存放在.data段中。

向量和内存缓存未命中

当我们通过指针或引用访问对象时,虚函数调度机制将会被激活。为了使用虚函数,开发人员通常会创建一个vector,其中包含一些指针,这些指针指向在堆上分配的对象。

当考虑性能时,这种方法有巨大的缺点,就是不能保证在堆上分配的对象是有序的。如果相邻的指针没有指向相邻的堆对象,这样会出现大量的缓存未命中,从而使程序执行变慢。

编译器优化

虚函数和非虚函数之间的区别是与编译器优化有关。请记住,虚函数的地址只有在运行时才知道。当我们迭代一组对象时,运行时系统必须找出每个对象的虚函数地址。

对于非虚函数,编译器知道非虚函数的地址。编译器可以将函数内联。这样可以节省函数调用和从函数返回的指令,但这并不是内联所做的唯一优化。

通过内联操作,编译器可以删除那些传递给函数但是并未使用过的参数,编译器可以将重复的操作移到循环之外,可以有效地向量化循环操作,可以执行软件流水线:例如,展开一个循环两次,内联同一个函数两次,然后将两个函数的操作交错,以便更好的利用硬件。

跳转目的地猜测和虚函数

现代硬件做了很多猜测。在调用虚函数的情况下,硬件通常直到很晚才知道跳转目的地。为了加速程序的执行,硬件通常会猜测目的地址,并在知道目标地址之前就开始执行指令。

如果猜对了,就继续执行;如果没有猜对,CPU将恢复到该点之前所做的工作,并开始执行来自正确目的地址的指令。错误的猜测会浪费时间并使程序变慢,许多错误的猜测会使程序变慢。

缓存驱逐和性能

现代硬件配备了很多缓存子系统,使其运行速度更快。有一种缓存是指令缓存:经常执行的指令保存在这个缓存中,因此可以更快地获取和解码它们。另一个缓存系统保存分支指令的比较结果:如果硬件可以从同一指令的先前执行中猜测分支的目的地,这可以加快计算速度。请注意,这些缓存的大小有限,当缓存没有位置存放目前的代码时,缓存将会删除旧代码的指令和分支预测结果。

考虑一个这样的场景,几个不同的类型实现了同一个虚函数的不同版本。你的程序将会迭代调用对象,并根据对象来调用不同实现的虚函数。如果并未对类型排序,则对虚函数的大多数调用将会导致对不同函数的调用。如果函数足够大,则当前函数中的指令将删除缓存中的旧指令。这样不断删除缓存中的数据,将会使程序变慢。

最后

虚函数本身并不是C++程序变慢的主要原因,但它们会导致一些不良行为,最终导致硬件利用率和性能不佳。

硬件希望的是可预测性:相同的类型,相同的功能,相邻的内存地址

如果确实需要使用虚函数,请注意以下几点:

  • 内存中对象的排列很重要。如果对象分散在内存中,则性能会变差。
  • 使小型函数非虚化。虚函数的大部分开销来源自小型函数,调用它们的成本高于执行它们的成本。除此之外,编译器擅长优化小型虚函数。
  • 保持对象按类型排序。这将有效的利用指令缓存和分支预测缓存,使代码更快。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值