寻找性能瓶颈,然后针对性地进行优化,逐个解决。应该按照层次进行,从上层慢慢深入到底层。
过早优化是万恶之源!
栈回溯和符号解析是绕不开的两个问题,通常都依赖于debuginfo(dwarf格式)。栈回溯当然也可以用最简单的基于frame pointer的回溯,但是只要开启优化,frame pointer大概会被省略掉。
火焰图层数越高,表明调用栈越深;顶层越宽,表明该函数的开销越大。
所有调试、性能分析和跟踪工具都依赖于某种逻辑注入(instrumentation)机制,可以是静态的(编译期进行),也可以动态的(运行期进行)。
- event based:根据若干事件触发
- sampling based:以一定频率采样调用栈
在性能分析前,可以利用time
命令先确定一个程序的运行时特性:
sys time就是在内核态执行系统调用花费的时间,而user time就是在用户态花费的时间。
- sys + usr ~ wall:说明程序是CPU密集型
- sys >> usr:尝试减少系统调用次数,或使用系统级性能分析工具
- sys << usr:使用用户态性能分析工具
- sys + usr << wall:说明程序并没有执行太多代码,有大量的off-cpu时间,是I/O密集型,应使用用户态跟踪工具
on-cpu, off-cpu(等待I/O、锁,睡眠)
¶perf
1 | # install on WSL2 |
原理:每隔一个固定的时间,就在CPU上产生一个中断,该中断收集程序计数器、整个调用栈。
PMU(性能监控单元)计数器可以用来统计L1 Cache失效的次数、分支预测失败的次数。
perf list
:列出所有能够触发perf采样点的事件(包括硬件事件(PMU产生),软件事件(进程切换),静态跟踪点)perf top
:实时观察CPU时间的开销perf stat
:运行指令,对程序运行过程中的性能计数器进行统计perf record
:收集采样信息,并保存到文件perf.data
中perf report
:根据收集的perf.data
显示分析结果perf script
:解析perf.data
文件,生成out.perf
,可用于生成火焰图
1 | perf record -F 99 -g -p $pid -- sleep $sec |
缺陷:
- 基于采样的方式
¶gperftools
在编译时添加选项-Wl,--no-as-needed,-lprofiler,--as-needed
。
./test_capture test_capture.prof --text > prof.txt
¶gprof
需要在编译时添加-pg
的编译选项,这会在每个函数的起始处静态注入一个对_mcount
函数的调用。
运行程序会生成一个gmon.out
文件。
gprof -b ./exe gmon.out > report.txt
缺陷:
- 不能用于多线程
- 必须要编译选项
- 仍然通过采样计算运行时间
- 不能实时输出结果,只能等程序运行完才能分析
¶papi
准确提供很多底层信息,如周期数、缓存未命中数;但需要自己写相关的代码。
¶uftrace
同样利用-pg
编译选项,通过overwrite掉__mcount
函数实现插桩。
-P.
选项还可以动态插桩。
缺陷:
- 不能实时输出结果,只能等程序运行完才能分析
¶Callgrind
是Valgrind中一个应用,本质上是一个虚拟机,通过模拟每条指令的执行以得到运行信息。
valgrind --tool=callgrind ./exe
会生成一个文件callgrind.out.pid
kcachegrind callgrind.out.pid
使用kcachedgrind
读取生成的文件会提供分析结果
缺陷:
- 由于是通过指令级别的模拟,overhead过大
¶strace/ltrace
strace跟踪系统调用,而ltrace跟踪库函数调用。
缺陷:
- 都是基于ptrace系统调用,开销较大。
¶ftrace
主要用于观测内核函数。其实也是利用了-pg
编译选项,不过在关闭时,_mcount
调用会变成nop指令,只有在启动时才会动态修改成对mcount的调用。
特别地,function graph会追踪子函数,因此在启动时必须让所有函数都调用mcount。
在mcount中可以记录进入时的信息,并篡改调用函数的返回地址,然后再正常执行调用函数,最后进入被篡改的返回地址处再记录退出时的信息,再回复正常执行的返回地址。
具体用法是与虚拟文件系统tracefs交互。
1 | mount -t tracefs tracefs /sys/kernel/tracing |
¶etrace
利用了编译选项-finstrument-functions
,该选项会在每个函数的入口和出口处增加一个hook函数__cyg_profile_func_enter(void *this_fn, void *call_site)
和__cyg_profile_func_exit(void *this_fn, void *call_site)
。
我们可以自己override这两个函数来实现观测。