C++程序性能控制
常见的C++性能约束有:cpu,内存,网络带宽,磁盘读写(iops)。
性能控制好的程序,也可以作为商用软件竞争的优势和亮点。
在本文中将从控制与监控两个方向介绍各个性能点的处置方式。
性能控制并不是万能的,而且多数控制方式仅能控制整个程序的最大值或者平均值,这并不是保证程序本身的性能占用就是合理的。无论哪种资源,都应该在方案设计阶段进行考虑,无论是处理过程中增加间歇,还是选择更加合理的数据结构等。
cpu
cpu的控制是这些性能资源中做好控制和观测的。可以直接通过cgroup进行限制,网上也有很多资料,所以本文仅简单介绍。
通过cgroup限制cpu占用的原理
从最简单的角度进行解释
- 什么是cpu占用?
cpu占用通常来说都是指的一个百分比的值,表示着一个程序占用了系统的多少性能。将系统的性能比做有长度的传送带,将一个程序放到传送带上运行,其占用了几米,这几米占总传送带长度的百分比就是cpu占用。 - 多核cpu如何理解?
现在的机器大多都是多核的了,也就有多个cpu,所以从概念上有两个cpu占用,分别是单核cpu占用和平均cpu占用。每个程序一般可以在任意一个cpu上运行,平均到每个cpu上运行的占比,就是平均cpu,同样这也是占总系统的百分比。类比到一个核上的占用就是单核cpu了,这个值是可以大于100%的。 - cgroup限制cpu的原理
拿传送带模型来说,在cpu的传送带上,每一百米,会有7米在运行我们的程序,那么cpu占用就是7%。而cgroup可以控制每一米运行哪个程序,所以这就很简单,想限制到多少都是可以的,只是在这其中1%就是限制的最小力度了。对比官方的概念,每一米就是一个时间片,在cgroup控制时,可以控制分子和分母,即[运行时间片个数]/[每经历多少个时间片的时间]。
综上所述,通过cgroup限制后,cpu就能基本保证是可控的了。
代码控制cpu占用
- 适当增加睡眠
- 在计算密集度高的位置,可能导致较高的cpu占用,可以适当增加sleep,控制长期占用cpu,挤占同程序其他模块的正常cpu活动。但这也是存在缺陷的,在编译阶段可能打断编译优化,在运行阶段(特别是在有gpu的机器上)会导致大量计算缓存被浪费。
- 为单个线程绑定特定的cpu运行
- 一个系统上可能有多个cpu,但这些cpu也可以是不同型号的,有的cpu计算能力更好,有的cpu读取缓存更快等。在C++程序上可以通过
pthread_setaffinity_np
将一个线程绑定到一个确定的cpu上运行。 - cpu的结构中是存在一些缓存的,如果指定一个线程运行还是有可能继续使用到这部分缓存,加快运行速度的。
- 调整合适的cgroup限制值
- 设置cgroup限制cpu时,需要提供两个值,表示百分比的分子和分母。如想设置为7%的cpu占用(单核)。这时可以设置「7,100」,也可以设置「70,1000」,「700,10000」等。分母越大,允许程序连续运行的时间越长,有利于使用到缓存加速。但相对的瞬时cpu更可能出现超过平均值很多的情况。
cpu监控
- cmd命令:top pidstat等都可以对cpu占用进行监控。另外,其他比较强力的工具还有
perf top -p 28764
,可以直接看到cpu占用发生在哪个阶段,可以看作很简略的火焰图 - proc目录下的stat文件中存在对应列,在通过计算后可以得到cpu。具体计算方式较为复杂,可以参考:https://tool.4xseo.com/a/50844.html
- 火焰图进行性能分析
内存
程序内存的控制不能向cpu那样,直接控制后就不会超了。通过cgroup限制,当程序内存超出限值后,杀死进程,这仅能保证此程序的运行不会影响到其他程序的运行。
cgroup限制内存异常场景:由于交换区被占满,导致系统卡死,虽然程序内存超限,但cgroup也需要很久才能检测到进程超内存。
代码控制内存占用
代码中控制内存占用的方式,其实也都很容易想到,程序中会占用内存的有几部分。
- 代码中申请使用的栈/堆等内存
- 加载的三方so所占用的内存
-
优化代码中申请的内存
- 使用合理的数据结构,并合理控制队列长度
- 尽量不使用new独立申请内存
- 减少太占用内存的结构,如过长的vector,Json,避免Json的值传递,超大的json等。
- 减少内存碎片的产生。适当使用内存池等,减少频繁的申请较大的内存。
-
减少三方so的内存占用
- 对于手动通过dlopen打开的so,可以考虑使用
RTLD_LAZY
类型加载so,减少簿部分内存占用。参考https://zhuanlan.zhihu.com/p/560349203 - 选择so时,可以考虑使用系统中自带的三方库,如果已有程序加载此so,也会与其共享这部分so的符号,减少部分内存占用
内存监控
内存监控有粗力度的,只监控进程上总体使用了多少内存。如使用pidstat命令-r参数查看,查看status文件中的值。也有可以用于内存泄漏问题定位的关注每个函数申请了多少内存,有没有被释放。后者需要编译带有符号表的程序,通过valgrind等工具进行检测,后面会补充一下如何细节定位内存泄漏问题,以及内存使用情况问题。
带宽
后补充
磁盘访问指的是读写磁盘,对于当下的磁盘来说,这读写次数/频率已经都不是问题了,但是对于老版的磁盘,可能还有这样的限制。
iops限制
-
可以通过cgroup对io读写进行限制,但这里有cgroup v1和v2版本的区别。对于v1版本,监控iops时,仅是能监控进程直接写入磁盘的,而通常我们使用write函数写入文件时,先会写到用户态的缓冲区,即使调用flush也只是刷新到内核的缓冲区,后续在内核线程的调度过程中,真正写入到磁盘中。这时候需要使用directio直接写入磁盘,才能被v1版本监控到io使用。而v2版本,在4.15版本的内核被引入,但默认使用的都还是v1版本。在v2版本中,会跟踪每一条访问,准确限制进程的io访问。
-
增加内存盘的方式,高频使用的文件存储到内存盘中,减少对磁盘的读写
iops的监控
可以使用iotop监控,或者perf命令监控一些挂载点的调用。某些情况下,也可以将整机的io使用,看作是单个进程的io占用