gitbook/罗剑锋的C++实战笔记/docs/248305.md

240 lines
15 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 18 | 性能分析:找出程序的瓶颈
你好我是Chrono。
今天是“技能进阶”单元的最后一节课,我也要兑现刚开始在“概论”里的承诺,讲一讲在运行阶段我们能做什么。
## 运行阶段能做什么
在编码阶段你会运用之前学习的各种范式和技巧写出优雅、高效的代码然后把它交给编译器。经过预处理和编译这两个阶段源码转换成了二进制的可执行程序就能够在CPU上“跑”起来。
在运行阶段C++静态程序变成了动态进程是一个实时、复杂的状态机由CPU全程掌控。但因为CPU的速度实在太快程序的状态又实在太多所以前几个阶段的思路、方法在这个时候都用不上。
所以,我认为,在运行阶段能做、应该做的事情主要有三件:**调试**Debug**、测试**Test**和性能分析**Performance Profiling
调试你一定很熟悉了常用的工具是GDB我在前面的“[轻松话题](https://time.geekbang.org/column/article/239599)”里也讲过一点它的使用技巧。它的关键是让高速的CPU慢下来把它降速到和人类大脑一样的程度于是我们就可以跟得上CPU的节奏理清楚程序的动态流程。
测试的目标是检验程序的功能和性能保证软件的质量它与调试是相辅相成的关系。测试发现Bug调试去解决Bug再返回给测试验证。好的测试对于软件的成功至关重要有很多现成的测试理论、应用、系统你可以参考下我就不多说了
一般来说,程序经过调试和测试这两个步骤,就可以上线运行了,进入第三个、也是最难的性能分析阶段。
什么是性能分析呢?
你可以把它跟Code Review对比一下。Code Review是一种静态的程序分析方法在编码阶段通过观察源码来优化程序、找出隐藏的Bug。而性能分析是一种动态的程序分析方法在运行阶段采集程序的各种信息再整合、研究找出软件运行的“瓶颈”为进一步优化性能提供依据指明方向。
从这个粗略的定义里,你可以看到,性能分析的关键就是“**测量**”,用数据说话。没有实际数据的支撑,优化根本无从谈起,即使做了,也只能是漫无目的的“不成熟优化”,即使成功了,也只是“瞎猫碰上死耗子”而已。
性能分析的范围非常广可以从CPU利用率、内存占用率、网络吞吐量、系统延迟等许多维度来评估。
今天我只讲多数时候最看重的CPU性能分析。因为CPU利用率通常是评价程序运行的好坏最直观、最容易获取的指标优化它是提升系统性能最快速的手段。而其他的几个维度也大多与CPU分析相关可以达到“以点带面”的效果。
## 系统级工具
刚才也说了,性能分析的关键是测量,而测量就需要使用工具,那么,你该选什么、又该怎么用工具呢?
其实Linux系统自己就内置了很多用于性能分析的工具比如top、sar、vmstat、netstat等等。但是Linux的性能分析工具太多、太杂有点“乱花渐欲迷人眼”的感觉想要学会并用在实际项目里不狠下一番功夫是不行的。
所以为了让你能够快速入门性能分析我根据我这些年的经验挑选了四个“高性价比”的工具top、pstack、strace和perf。它们用起来很简单而且实用性很强可以观测到程序的很多外部参数和内部函数调用由内而外、由表及里地分析程序性能。
第一个要说的是“**top**”它通常是性能分析的“起点”。无论你开发的是什么样的应用程序敲个top命令就能够简单直观地看到CPU、内存等几个最关键的性能指标。
top展示出来的各项指标的含义都非常丰富我来说几个操作要点吧帮助你快速地抓住它的关键信息。
一个是按“M”看内存占用RES/MEM另一个是按“P”看CPU占用这两个都会从大到小自动排序方便你找出最耗费资源的进程。
另外你也可以按组合键“xb”然后用“<>”手动选择排序的列,这样查看起来更自由。
我曾经做过一个“魔改”Nginx的实际项目下面的这个截图展示的就是一次top查看的性能
![](https://static001.geekbang.org/resource/image/6a/a8/6a44808ccc8b1df7bef0a51c888ce2a8.png)
从top的输出结果里你可以看到进程运行的概况知道CPU、内存的使用率。如果你发现某个指标超出了预期就说明可能存在问题接下来你就应该采取更具体的措施去进一步分析。
比如说这里面的一个进程CPU使用率太高我怀疑有问题那我就要深入进程内部看看到底是哪些操作消耗了CPU。
这时,我们可以选用两个工具:**pstack和strace**。
pstack可以打印出进程的调用栈信息有点像是给正在运行的进程拍了个快照你能看到某个时刻的进程里调用的函数和关系对进程的运行有个初步的印象。
下面这张截图显示了一个进程的部分调用栈可以看到跑了好几个ZMQ的线程在收发数据
![](https://static001.geekbang.org/resource/image/6c/9c/6c115ce03d6b4803960277468cf91b9c.png)
不过pstack显示的只是进程的一个“静态截面”信息量还是有点少而strace可以显示出进程的正在运行的系统调用实时查看进程与系统内核交换了哪些信息
![](https://static001.geekbang.org/resource/image/b7/f0/b747d0d977c7f420507ec9e9d84e6ff0.png)
把pstack和strace结合起来你大概就可以知道进程在用户空间和内核空间都干了些什么。当进程的CPU利用率过高或者过低的时候我们有很大概率能直接发现瓶颈所在。
不过,有的时候,你也可能会“一无所获”,毕竟这两个工具获得的信息只是“表象”,数据的“含金量”太低,做不出什么有效的决策,还是得靠“猜”。要拿到更有说服力的“数字”,就得**perf**出场了。
perf可以说是pstack和strace的“高级版”它按照固定的频率去“采样”相当于连续执行多次的pstack然后再统计函数的调用次数算出百分比。只要采样的频率足够大把这些“瞬时截面”组合在一起就可以得到进程运行时的可信数据比较全面地描述出CPU使用情况。
我常用的perf命令是“**perf top -K -p xxx**”按CPU使用率排序只看用户空间的调用这样很容易就能找出最耗费CPU的函数。
比如下面这张图显示的是大部分CPU时间都消耗在了ZMQ库上其中内存拷贝调用居然达到了近30%,是不折不扣的“大户”。所以,只要能把这些拷贝操作减少一点,就能提升不少性能。
![](https://static001.geekbang.org/resource/image/55/15/5543dec44c23d23b583bc937213e7c15.png)
总之,**使用perf通常可以快速定位系统的瓶颈帮助你找准性能优化的方向**。课下你也可以自己尝试多分析各种进程比如Redis、MySQL等等观察它们都在干什么。
## 源码级工具
top、pstack、strace和perf属于“非侵入”式的分析工具不需要修改源码就可以在软件的外部观察、收集数据。它们虽然方便易用但毕竟是“隔岸观火”还是不能非常细致地分析软件效果不是太理想。
所以,我们还需要有“侵入”式的分析工具,在源码里“埋点”,直接写特别的性能分析代码。这样针对性更强,能够有目的地对系统的某个模块做精细化分析,拿到更准确、更详细的数据。
其实,这种做法你并不陌生,比如计时器、计数器、关键节点打印日志,等等,只是通常并没有上升到性能分析的高度,手法比较“原始”。
在这里,我要推荐一个专业的源码级性能分析工具:**Google Performance Tools**一般简称为gperftools。它是一个C++工具集里面包含了几个专门的性能分析工具还有一个高效的内存分配器tcmalloc分析效果直观、友好、易理解被广泛地应用于很多系统经过了充分的实际验证。
```
apt-get install google-perftools
apt-get install libgoogle-perftools-dev
```
gperftools的性能分析工具有CPUProfiler和HeapProfiler两种用来分析CPU和内存。不过如果你听从我的建议总是使用智能指针、标准容器不使用new/delete就完全可以不用关心HeapProfiler。
CPUProfiler的原理和perf差不多也是按频率采样默认是每秒100次100Hz也就是每10毫秒采样一次程序的函数调用情况。
它的用法也比较简单,只需要在源码里添加三个函数:
* **ProfilerStart()**,开始性能分析,把数据存入指定的文件里;
* **ProfilerRegisterThread()**,允许对线程做性能分析;
* **ProfilerStop()**,停止性能分析。
所以你只要把想做性能分析的代码“夹”在这三个函数之间就行运行起来后gperftools就会自动产生分析数据。
为了写起来方便我用shared\_ptr实现一个自动管理功能。这里利用了void\*和空指针可以在智能指针析构的时候执行任意代码简单的RAII惯用法
```
auto make_cpu_profiler = // lambda表达式启动性能分析
[](const string& filename) // 传入性能分析的数据文件名
{
ProfilerStart(filename.c_str()); // 启动性能分析
ProfilerRegisterThread(); // 对线程做性能分析
return std::shared_ptr<void>( // 返回智能指针
nullptr, // 空指针,只用来占位
[](void*){ // 删除函数执行停止动作
ProfilerStop(); // 停止性能分析
}
);
};
```
下面我写一小段代码,测试正则表达式处理文本的性能:
```
auto cp = make_cpu_profiler("case1.perf"); // 启动性能分析
auto str = "neir:automata"s;
for(int i = 0; i < 1000; i++) { // 循环一千次
auto reg = make_regex(R"(^(\w+)\:(\w+)$)");// 正则表达式对象
auto what = make_match();
assert(regex_match(str, what, reg)); // 正则匹配
}
```
注意我特意在for循环里定义了正则对象现在就可以用gperftools来分析一下这样做是不是成本很高。
编译运行后会得到一个“case1.perf”的文件里面就是gperftools的分析数据但它是二进制的不能直接查看如果想要获得可读的信息还需要另外一个工具脚本pprof。
但是pprof脚本并不含在apt-get的安装包里所以你还要从[GitHub](https://github.com/gperftools/gperftools)上下载源码,然后用“`--text`”选项,就可以输出文本形式的分析报告:
```
git clone git@github.com:gperftools/gperftools.git
pprof --text ./a.out case1.perf > case1.txt
Total: 72 samples
4 5.6% 5.6% 4 5.6% __gnu_cxx::__normal_iterator::base
4 5.6% 11.1% 4 5.6% _init
4 5.6% 16.7% 4 5.6% std::vector::begin
3 4.2% 20.8% 4 5.6% __gnu_cxx::operator-
3 4.2% 25.0% 5 6.9% std::__distance
2 2.8% 27.8% 2 2.8% __GI___strnlen
2 2.8% 30.6% 6 8.3% __GI___strxfrm_l
2 2.8% 33.3% 3 4.2% __dynamic_cast
2 2.8% 36.1% 2 2.8% __memset_sse2
2 2.8% 38.9% 2 2.8% operator new[]
```
pprof的文本分析报告和perf的很像也是列出了函数的采样次数和百分比但因为是源码级的采样会看到大量的内部函数细节虽然很详细但很难找出重点。
好在pprof也能输出图形化的分析报告支持有向图和火焰图需要你提前安装Graphviz和FlameGraph
```
apt-get install graphviz
git clone git@github.com:brendangregg/FlameGraph.git
```
然后,你就可以使用“`--svg`”“`--collapsed`”等选项,生成更直观易懂的图形报告了:
```
pprof --svg ./a.out case1.perf > case1.svg
pprof --collapsed ./a.out case1.perf > case1.cbt
flamegraph.pl case1.cbt > flame.svg
flamegraph.pl --invert --color aqua case1.cbt > icicle.svg
```
我就拿最方便的火焰图来“看图说话”吧。你也可以在[GitHub](https://github.com/chronolaw/cpp_study/blob/master/section4/icicle.svg)上找到原图。
![](https://static001.geekbang.org/resource/image/75/30/7587a411eb9c7a16f68bd3453a1eec30.png)
这张火焰图实际上是“倒置”的冰柱图,显示的是自顶向下查看函数的调用栈。
由于C++有名字空间、类、模板等特性函数的名字都很长看起来有点费劲不过这样也比纯文本要直观一些可以很容易地看出正则表达式占用了绝大部分的CPU时间。再仔细观察的话就会发现\_Compiler()这个函数是真正的“罪魁祸首”。
找到了问题所在,现在我们就可以优化代码了,把创建正则对象的语句提到循环外面:
```
auto reg = make_regex(R"(^(\w+)\:(\w+)$)"); // 正则表达式对象
auto what = make_match();
for(int i = 0; i < 1000; i++) { // 循环一千次
assert(regex_match(str, what, reg)); // 正则匹配
}
```
再运行程序你会发现程序瞬间执行完毕而且因为优化效果太好gperftools甚至都来不及采样不会产生分析数据。
基本的gperftools用法就这么多了你可以再去看它的[官方文档](https://github.com/gperftools/gperftools/tree/master/docs)了解更多的用法比如使用环境变量和信号来控制启停性能分析或者链接tcmalloc库优化C++的内存分配速度。
## 小结
好了今天主要讲了运行阶段里的性能分析它能够回答为什么系统“不够好”not good enough而调试和测试回答的是为什么系统“不好”not good
简单小结一下今天的内容:
1. 最简单的性能分析工具是top可以快速查看进程的CPU、内存使用情况
2. pstack和strace能够显示进程在用户空间和内核空间的函数调用情况
3. perf以一定的频率采样分析进程统计各个函数的CPU占用百分比
4. gperftools是“侵入”式的性能分析工具能够生成文本或者图形化的分析报告最直观的方式是火焰图。
性能分析与优化是一门艰深的课题也是一个广泛的议题CPU、内存、网络、文件系统、数据库等等每一个方向都可以再引出无数的话题。
今天介绍的这些是我挑选的对初学者最有用的内容学习难度不高容易上手见效快。希望你能以此为契机在今后的日子里多用、多实际操作并且不断去探索、应用其他的分析工具综合运用它们给程序“把脉”才能让C++在运行阶段跑得更好更快更稳,才能不辜负前面编码、预处理和编译阶段的苦心与努力。
## 课下作业
最后还是留两个思考题吧:
1. 你觉得在运行阶段还能够做哪些事情?
2. 你有性能分析的经验吗?听了今天的这节课之后,你觉得什么方式比较适合自己?
欢迎你在留言区写下你的思考和答案,如果觉得今天的内容对你有所帮助,也欢迎分享给你的朋友。我们下节课见。
![](https://static001.geekbang.org/resource/image/45/f1/45adfe31c60a89ff54b7dbce366e2bf1.png)