好耶!是小对象优化

论静态优化在C++程序设计中的应用

说到小对象优化,今年开始跟着我上课的朋友可能有一点印象,这是因为我在课程计划中提到了如何实现小对象优化,但后来由于课程进度等原因反正就是没讲,今天就说一下什么是小对象优化

自计算机诞生以来静态和动态就是非常矛盾的一对存在,静态的东西速度快,动态的东西功能强,因此静态化就是程序优化中非常重要的一个方面。今天我就打算利用任意容器(any)来讨论一下怎样给动态的东西进行静态优化

任意容器的原理和实现

任意容器在C++17标准中被纳入STL的范畴,这在一定程度上也凸显了任意容器这一原理简单且实用的工具的重要性

任意容器的原理说起来也非常简单,即类型擦除(Type Erasing),利用面向对象编程中的抽象方法将具体的类型信息全部“擦除”,具体来说就是将所有的类型全部抽象为一个Object

但这个时候就引出一个问题:为了达到这种效果,派生类对象往往会在堆上分配

在堆上分配就带来了两个显著的问题:

  • 首先是频繁分配带来的内存碎片问题
  • 其次是堆内存分配本身就是低性能的

其实这里内存碎片带来的主要问题其实还是性能,也就是说我们现在还是最关心一个问题就是性能

堆内存分配的性能优化

一般来说常用的堆内存分配性能优化手段有以下几种:

  1. 内存池,在大型项目中比较常见,也就是说自己维护内存的分配
  2. 缓冲池,预缓存一部分内存提供给IO热点,在CovScript 3的Any中得到了应用
  3. 缓式求值(Lazy Evaluation),利用写时复制(Copy On Write)等手段减少不必要的内存分配

这几种优化手段的优点显而易见,但也都有其缺点:

  1. 内存池往往对分配算法要求较高,且一次性会占用过多系统内存
  2. IO热点难以判断,可能会在IO不频繁的场景下反过来拖累性能
  3. 缓式求值传统上使用引用计数器和句柄实现,引用计数器会造成额外的性能开销,而句柄实现的间接内存访问也会破坏编译器的内联优化,在Modern C++中移动语义在一定程度上替代了缓式求值的地位

所以,最好的性能优化方案,其实是避免堆内存分配。

老李又开始说废话了,如果能用栈内存,谁想用堆内存啊!!!

曲线救国之小对象优化:引论

既然堆内存不好玩,为什么不搞点栈内存玩呢?

问题:能不能在栈内存上搞动态内存分配?

在这里请读者回顾一下C++中Placement New的相关语法

也就是说,C++可以在一块特定的内存上构造任意对象,这块内存既然可以是堆内存,当然也可以是栈内存,那么我们只需要拿到一块堆内存就可以了

但说到这里很多朋友就要开始挠头了,获取栈内存可以用malloc,堆内存用什么?

答案是数组

用数组的原因显而易见,单个变量所占用的堆内存往往大小有限

在这里我们常使用unsigned char作为数组的基本类型单位,因为unsigned char是C++中占用内存最小的内建类型,也就是八个字节,因此我们可以用unsigned char*合法的表示任何C++中的内存地址。

现在我们拿到了栈内存,也知道怎么往栈内存里面塞东西,那么接下来呢?

曲线救国之小对象优化:时间与空间的权衡

栈内存看起来好哇!那为什么大家不把整个程序的数据都存储在栈内存里面呢?

首先,栈内存一旦分配,大小是不能改变的(此处不考虑VLA变长数组)

其次,很多情况下内存的拷贝性能是比较差的,因此要考虑栈内存对对象大小的影响

再说深一点,过大的对象会导致Cache Miss,这样反而会得不偿失

什么是Cache Miss?

现代CPU往往通过多级缓存的方式提高内存访问的性能,然而缓存的大小是十分有限的,因此过大的对象可能导致CPU无法通过缓存加速其访问,因此会一定程度上影响性能

到这里我们就需要考虑一个恒久的话题了:怎样权衡时间和空间

较大的对象我们只能扔到堆里,从而避免浪费宝贵的栈空间

而较小的对象,我们就可以将其存储在栈里

但这个阈值怎么样来确定呢?是不是一股脑把所有的数据全部存储在栈上就能达到最佳性能?

过小的阈值是不是没有实际意义?毕竟如果装不了什么东西,到最后还是要扔到堆上

过大的阈值会不会浪费内存空间?毕竟大多数情况下大家都会选择将较大的内存块扔到堆里面

根据大量的测试,我发现3*sizeof(void*)是最合适的大小,巧合的是STL也选择了这个阈值

这个结果是综合了内存对齐和平均情况下内存的占用情况而定的,在实际使用中读者可以尝试自己调整一下阈值看看效果

曲线救国之小对象优化:实现

传统情况下any只需存储一个Object基类指针即可,引入小对象优化就带来了很多问题

  1. 存储小对象的Buffer应该放在哪里?
  2. 数据回收时如何处理不同的回收语义?
  3. 小对象复制时能不能直接按位复制?
  4. 怎样处理移动语义?

这里让我们一一进行讨论

发表回复