性能分析

(6 mins to read)

寻找性能瓶颈,然后针对性地进行优化,逐个解决。应该按照层次进行,从上层慢慢深入到底层。

过早优化是万恶之源!

栈回溯和符号解析是绕不开的两个问题,通常都依赖于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
2
3
4
5
6
# install on WSL2
sudo apt install flex bison
git clone https://github.com/microsoft/WSL2-Linux-Kernel --depth 1
cd WSL2-Linux-Kernel/tools/perf
make -j8
sudo cp perf /usr/local/bin

原理:每隔一个固定的时间,就在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
2
3
perf record -F 99 -g -p $pid -- sleep $sec
perf script | ./stackcollapse-perf.pl > out.perf-folded
./flamegraph.pl out.perf-folded > perf.svg

缺陷:

  • 基于采样的方式

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
2
3
4
5
6
7
8
9
10
mount -t tracefs tracefs /sys/kernel/tracing
ls /sys/kernel/tracing/
cd /sys/kernel/tracing
echo nop > current_tracer
echo function > current_tracer
echo function_graph > current_tracer
echo ext4_* > set_ftrace_filter # 只跟踪
echo ext4_* > set_ftrace_notrace # 不跟踪
echo $PID > set_ftrace_pid
cat trace

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这两个函数来实现观测。

pprof

bpftrace

dtrace

systemtap