Bootstrap

C++程序性能控制(cpu,内存,带宽,io)

C++程序性能控制

常见的C++性能约束有:cpu,内存,网络带宽,磁盘读写(iops)。
性能控制好的程序,也可以作为商用软件竞争的优势和亮点。

在本文中将从控制与监控两个方向介绍各个性能点的处置方式。

性能控制并不是万能的,而且多数控制方式仅能控制整个程序的最大值或者平均值,这并不是保证程序本身的性能占用就是合理的。无论哪种资源,都应该在方案设计阶段进行考虑,无论是处理过程中增加间歇,还是选择更加合理的数据结构等。

cpu

cpu的控制是这些性能资源中做好控制和观测的。可以直接通过cgroup进行限制,网上也有很多资料,所以本文仅简单介绍。

通过cgroup限制cpu占用的原理

从最简单的角度进行解释

  1. 什么是cpu占用?
    cpu占用通常来说都是指的一个百分比的值,表示着一个程序占用了系统的多少性能。将系统的性能比做有长度的传送带,将一个程序放到传送带上运行,其占用了几米,这几米占总传送带长度的百分比就是cpu占用。
  2. 多核cpu如何理解?
    现在的机器大多都是多核的了,也就有多个cpu,所以从概念上有两个cpu占用,分别是单核cpu占用和平均cpu占用。每个程序一般可以在任意一个cpu上运行,平均到每个cpu上运行的占比,就是平均cpu,同样这也是占总系统的百分比。类比到一个核上的占用就是单核cpu了,这个值是可以大于100%的。
  3. cgroup限制cpu的原理
    拿传送带模型来说,在cpu的传送带上,每一百米,会有7米在运行我们的程序,那么cpu占用就是7%。而cgroup可以控制每一米运行哪个程序,所以这就很简单,想限制到多少都是可以的,只是在这其中1%就是限制的最小力度了。对比官方的概念,每一米就是一个时间片,在cgroup控制时,可以控制分子和分母,即[运行时间片个数]/[每经历多少个时间片的时间]。

综上所述,通过cgroup限制后,cpu就能基本保证是可控的了。

代码控制cpu占用

  1. 适当增加睡眠
    • 在计算密集度高的位置,可能导致较高的cpu占用,可以适当增加sleep,控制长期占用cpu,挤占同程序其他模块的正常cpu活动。但这也是存在缺陷的,在编译阶段可能打断编译优化,在运行阶段(特别是在有gpu的机器上)会导致大量计算缓存被浪费。
  2. 为单个线程绑定特定的cpu运行
    • 一个系统上可能有多个cpu,但这些cpu也可以是不同型号的,有的cpu计算能力更好,有的cpu读取缓存更快等。在C++程序上可以通过pthread_setaffinity_np将一个线程绑定到一个确定的cpu上运行。
    • cpu的结构中是存在一些缓存的,如果指定一个线程运行还是有可能继续使用到这部分缓存,加快运行速度的。
  3. 调整合适的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所占用的内存
  1. 优化代码中申请的内存

    • 使用合理的数据结构,并合理控制队列长度
    • 尽量不使用new独立申请内存
    • 减少太占用内存的结构,如过长的vector,Json,避免Json的值传递,超大的json等。
    • 减少内存碎片的产生。适当使用内存池等,减少频繁的申请较大的内存。
  2. 减少三方so的内存占用

    • 对于手动通过dlopen打开的so,可以考虑使用RTLD_LAZY类型加载so,减少簿部分内存占用。参考https://zhuanlan.zhihu.com/p/560349203
    • 选择so时,可以考虑使用系统中自带的三方库,如果已有程序加载此so,也会与其共享这部分so的符号,减少部分内存占用

内存监控

内存监控有粗力度的,只监控进程上总体使用了多少内存。如使用pidstat命令-r参数查看,查看status文件中的值。也有可以用于内存泄漏问题定位的关注每个函数申请了多少内存,有没有被释放。后者需要编译带有符号表的程序,通过valgrind等工具进行检测,后面会补充一下如何细节定位内存泄漏问题,以及内存使用情况问题。

带宽

后补充

磁盘访问(iops)

磁盘访问指的是读写磁盘,对于当下的磁盘来说,这读写次数/频率已经都不是问题了,但是对于老版的磁盘,可能还有这样的限制。

iops限制

  1. 可以通过cgroup对io读写进行限制,但这里有cgroup v1和v2版本的区别。对于v1版本,监控iops时,仅是能监控进程直接写入磁盘的,而通常我们使用write函数写入文件时,先会写到用户态的缓冲区,即使调用flush也只是刷新到内核的缓冲区,后续在内核线程的调度过程中,真正写入到磁盘中。这时候需要使用directio直接写入磁盘,才能被v1版本监控到io使用。而v2版本,在4.15版本的内核被引入,但默认使用的都还是v1版本。在v2版本中,会跟踪每一条访问,准确限制进程的io访问。

  2. 增加内存盘的方式,高频使用的文件存储到内存盘中,减少对磁盘的读写

iops的监控

可以使用iotop监控,或者perf命令监控一些挂载点的调用。某些情况下,也可以将整机的io使用,看作是单个进程的io占用

;