背景
当程序存在cpu性能问题时,我们需要找到是哪个函数占用较多的CPU,也就是找出热点函数;perf的火焰图就是这个用途
安装
在Linux系统中,perf 是 Linux 内核提供的性能分析工具,它通常包含在内核源代码包中。大多数现代的Linux发行版都提供了预编译的 perf 包,可以直接通过系统的包管理器安装。
根据你所使用的Linux发行版,以下是一些不同发行版上安装 perf 的指令:
对于基于Debian的系统(比如Ubuntu):
bash
sudo apt-get update
sudo apt-get install linux-tools-common linux-tools-generic linux-tools-`uname -r`
请注意,linux-tools-generic 包可能会安装不与你当前运行的内核版本完全匹配的 perf 版本。为了确保版本对应,使用 uname -r
来获取并安装与当前运行内核版本对应的 perf 版本。
对于基于Red Hat的系统(比如Fedora和CentOS):
对于 Fedora 你可以使用:
bash
sudo dnf install perf
对于CentOS或者RHEL,可以使用:
bash
sudo yum install perf
对于Arch Linux:
bash
sudo pacman -S linux-tools
在安装后,你可以通过键入 perf --version 来检查 perf 是否成功安装以及其版本信息。
如果你需要更高级的功能,或者你的系统没有预编译的 perf 可用,你也可以选择从Linux内核源代码编译 perf 工具。这个过程较为复杂,需要下载相应版本的Linux内核源代码,并按照源代码中的说明来编译 perf。
样例代码
设计如下的程序:有两个线程,分别运行func1->func1_1(初始化大小为10的vector,0-9)、func1_2(初始化大小为100的vector,0-99); func2->func2_1(初始化大小为1000的vector,0-999)、func2_2(初始化大小为10000的vector,0-9999)
cpp
#include <iostream>
#include <thread>
#include <vector>
void func1_1() {
std::vector<int> v(10);
int i = 0;
for(auto e : v) {
e = i++;
}
}
void func1_2() {
std::vector<int> v(100);
int i = 0;
for(auto e : v) {
e = i++;
}
}
void func2_1() {
std::vector<int> v(1000);
int i = 0;
for(auto e : v) {
e = i++;
}
}
void func2_2() {
std::vector<int> v(10000);
int i = 0;
for(auto e : v) {
e = i++;
}
}
void func1() {
while(true) {
func1_1();
func1_2();
}
}
void func2() {
while(true) {
func2_1();
func2_2();
}
}
int main() {
std::thread t1(&func1);
std::thread t2(&func2);
if(t1.joinable()) {
t1.join();
}
if(t2.joinable()) {
t2.join();
}
return 0;
}
编译
bash
g++ perf_demo.cpp -o perf_demo -lpthread
采集perf数据
-
找到要采集程序的pid
-
使用perf record命令来收集程序运行时的性能数据
bashperf record -F 99 -p 12519 -g -- sleep 30
参数解释如下:
- -F 99:指定采样频率(99 次/秒),可以根据需要调整该值。
- -p:要采集的进程pid。
- -g:记录调用栈信息,这对于生成火焰图是必需的。
- -- sleep 30:收集性能数据的持续时间,这里设置为30秒。sleep 30 表示在收集数据后暂停30秒,以确保 perf 有足够的时间来记录信息。
-
然后会在当前目录创建一个名 perf.data的文件,其中包含了采样数据
-
生成火焰图
-
下载安装FlameGraph工具,它是一组脚本,用来从 perf 的采样数据生成可视化的火焰图。
安装方式:
shellgit clone https://github.com/brendangregg/FlameGraph.git
-
按照前面提到的步骤,首先安装并使用 FlameGraph 工具转换采样数据
shellperf script | /path/to/FlameGraph/stackcollapse-perf.pl > out.perf-folded
-
使用转换后的数据生成SVG火焰图
shell/path/to/FlameGraph/flamegraph.pl out.perf-folded > perf_demo.svg
-
火焰图分析
火焰图提供了程序执行过程中的调用栈样本的可视化表示,它以一种直观的方式展示了哪些函数消耗了最多的CPU时间。下面是如何分析火焰图的一些基本步骤和技巧:
- 理解火焰图布局
- 水平方向:显示了采样到的栈帧。每个横向的条块代表一个函数调用,其宽度代表该函数在采样中占据的相对时间比例。宽度越大,说明函数占用的CPU时间越长。
- 垂直方向:展示了调用栈的深度。栈底部(图的最底层)通常是主函数或线程的起始点,更上层则代表函数的调用关系(被调用者在调用者之上)。
- 分析热点(Hot Spots)
找到最宽的几个柱形区域,这些通常是性能的热点,即你的程序花费时间最多的地方。有时候,一个宽柱形可能是因为单个函数调用自身就很耗时,有时则是由于该函数被频繁调用。 - 检查调用路径
从任意一个宽柱形开始,向上追踪它的调用路径。这可以帮助你理解为什么一个函数会变成热点。可能是它自身操作复杂,或者它被一个循环结构频繁调用。 - 注意递归调用
如果你看到在火焰图中有重复的模式,这可能表示递归调用。递归调用可能是性能问题的来源,特别是当递归没有正确优化时。 - 考虑不同层级的影响
尽量避免只关注顶部的函数。如果一个底层函数(位于火焰图较低位置)相对较宽,它可能被多个不同的上层函数调用,改进这样一个底层函数可能对整个程序性能有显著的提升。 - 比较前后变化
如果你在进行性能优化工作,应该在更改代码后重新生成火焰图,然后将新旧火焰图进行比较。这将帮助你直观地看到更改对性能的影响。 - 关注宽度变化
如果你注意到某个函数的子调用中有明显的宽度变化,这可能表示性能问题。它可能是计算密集型操作,或者存在条件判断导致的执行路径变化。
根据上面样例抓取的火焰图进行分析:
- func1(50%) 和 func2(50%) 宽度各占一半 -- 两个线程,分别占使用cpu的一半
- func1_1(7%)、func1_2(36%)、func2_1(4%)、func2_2(39%)
- 可以得出来,热点函数是func1_2、func2_2
perf的原理
perf 是 Linux 内核提供的性能分析工具,可以用来进行系统级和进程级的性能分析。它使用了Linux内核中的性能计数器子系统 perf_events。以下是 perf 工具的基本工作原理:
-
性能计数器(Hardware Performance Counters)
现代的CPU通常都带有硬件性能计数器,这些计数器可以在不影响系统性能的情况下,跟踪处理器内部事件,如指令执行次数、缓存命中/未命中次数等。
-
perf_events 内核子系统
perf 基于Linux内核中的 perf_events 子系统,后者提供了一种统一的接口来访问CPU硬件计数器以及其他监控事件,比如软件事件(上下文切换、页面缺失)和追踪点(tracepoints)。perf_events 子系统支持各种架构,并且是可扩展的。
-
事件采样(Sampling)
通过 perf record 命令时,perf 会配置 perf_events 开始收集数据。通常是基于事件采样的方式进行的,即按照一定频率记录特定事件发生的时候的系统状态。当硬件计数器达到预设的阈值(采样频率),就会生成一个中断,perf 便在这个时候捕获当前的程序计数器(PC)、调用栈和其他寄存器的值等信息。
-
记录和分析
采样得到的数据被写入到一个文件中(默认为 perf.data)。perf record 命令完成后,可以使用 perf report 查看和分析性能数据,或者使用 perf script 将数据导出供其他工具(如 FlameGraph)进一步分析。
-
调用栈捕获
当启用 -g 选项时,perf 不仅会记录下触发采样的那一刻的程序计数器位置,还会尝试捕获当前的调用栈。这对于理解程序行为至关重要,尤其是识别哪些函数调用导致了大量的CPU使用。
-
CPU和内核支持
由于 perf 直接与CPU硬件和内核模块交互,它需要CPU支持特定的性能计数器特性,同时也需要内核支持 perf_events 接口。因此,旧的CPU或者特定的内核编译配置可能无法使用 perf 的某些功能。
综合以上,perf 的原理是通过硬件性能计数器和 perf_events 内核子系统,提供了一种方法来测量和分析系统运行时的详细性能指标,从而使开发者能够深入理解系统的瓶颈和性能问题所在。
一些思考
可以参考perf的设计,对自己的模块设计性能采集和分析工具,profiling工具。