perf是 Linux 内核自带的性能分析工具,基于内核perf_event子系统,无需改代码、运行时开销低- 两大能力:计数(
perf stat,统计事件发生次数)与采样(perf record/report,定时抓取调用栈) - 常用子命令:
stat(计数器)、record(采样)、report(查看热点)、top(实时)、annotate(指令级)、script(原始数据/火焰图) - 核心指标:
instructions/cycles即 IPC(每周期指令数),越高越好;cache-misses/cache-references为缓存未命中率 - 火焰图链路:
perf script→stackcollapse-perf.pl→flamegraph.pl,把热点函数可视化 - C++ 采样前用
-g -fno-omit-frame-pointer编译,或用perf record --call-graph dwarf应对优化后帧指针缺失
perf(Performance Counters for Linux)是随 Linux 内核发布的性能剖析工具,工作在用户态与内核态之间,直接读取硬件性能计数器(PMU,Performance Monitoring Unit)与内核 tracepoint。相比gprof(需重编译、插桩开销大)和valgrind(运行速度慢几十倍),perf采样开销通常在 1%–5%,可用于生产环境在线排查。它属于linux-tools包,装好内核即可用。相关概念在 03-Resources/cpp/C++ Linux 编程 的「perf 如何分析性能瓶颈」一节有速览。
什么是 perf
perf 由两部分组成:内核态的 perf_event 子系统(负责访问 CPU 硬件计数器、软件事件、内核 tracepoint)和用户态的 perf 命令行工具。它通过 PMU 硬件计数器 统计 CPU 周期、指令数、缓存命中/未命中、分支预测等事件。
工作方式分两类:
- 计数(counting):
perf stat,程序运行期间累计某事件发生多少次,得到 IPC、缓存未命中率等宏观指标,判断「慢在哪一类」。 - 采样(sampling):
perf record,以固定频率(如 99Hz)打断程序、记录当时的调用栈,事后perf report聚合出「哪些函数被采样到最多次」——即 CPU 热点。
安装与权限配置
安装
# Debian / Ubuntu
sudo apt install linux-tools-common linux-tools-generic linux-tools-$(uname -r)
# CentOS / RHEL / Fedora
sudo yum install perf验证:
perf --version # 版本
perf test # 自检权限
非 root 用户采样受 /proc/sys/kernel/perf_event_paranoid 控制,数值越大限制越严:
| 值 | 权限含义(对非特权用户) |
|---|---|
-1 | 允许使用几乎所有事件(含内核事件),忽略 mlock 限制 |
0 | 禁止访问 tracepoint 原始数据 |
1 | 禁止 CPU 级(PMU 硬件)事件,仅允许软件事件 |
2 | 禁止内核态采样(只能采样用户态) |
3 | 禁止所有事件(Ubuntu 22.04 起默认,非 root 几乎用不了) |
# 查看当前值
cat /proc/sys/kernel/perf_event_paranoid
# 临时放开(生产环境谨慎)
sudo sysctl -w kernel.perf_event_paranoid=-1
# 查看内核符号是否对非 root 隐藏(0=可见,1=隐藏,2=完全隐藏)
cat /proc/sys/kernel/kptr_restrict
降低 perf_event_paranoid 会允许普通用户读取内核符号与 tracepoint 原始数据,可能泄露内核地址(KASLR 绕过)。个人开发机可放开到 -1 或 1;服务器仅对授权用户放开。
编译时准备(C++)
采样要看到函数名和调用栈,需满足:
# 必须有调试符号(-g 不影响优化,可放心加)
g++ -O2 -g main.cpp -o app
# 使用默认帧指针回溯时,别让编译器省略帧指针
g++ -O2 -g -fno-omit-frame-pointer main.cpp -o app-O2 -g 可同时获得「优化后的真实代码」和「源码行号」;若仍想要精确栈,用 --call-graph dwarf(见下文)替代帧指针方案,则无需 -fno-omit-frame-pointer。
核心子命令一览
| 子命令 | 作用 | 类比 |
|---|---|---|
perf list | 列出本机可用的事件(硬件/软件/缓存/tracepoint) | — |
perf stat | 计数统计,运行程序并汇总事件次数 | 宏观体检 |
perf record | 采样,记录调用栈到 perf.data | 抓现场 |
perf report | 读取 perf.data,交互查看热点函数 | 分析现场 |
perf top | 实时监控系统中 CPU 占用最高的函数 | 类似 top |
perf annotate | 把热点函数反汇编,标注每条指令的采样占比 | 汇编级定位 |
perf script | 导出原始采样数据,供脚本/火焰图处理 | 数据导出 |
perf diff | 对比两个 perf.data,看优化前后差异 | diff |
perf trace | 追踪系统调用,类似 strace 但开销更低 | 系统调用视角 |
perf bench | 内建微基准测试(内存/调度等) | 基准 |
perf list:列出可用事件
perf list # 全部事件
perf list hardware # 硬件事件(PMU 计数器)
perf list software # 软件事件
perf list cache # 缓存事件
perf list tracepoint # 内核 tracepoint(量大,建议 grep 过滤)
perf list | grep -E 'branch|cache-miss' # 按需过滤常用事件速查:
| 事件 | 类型 | 含义 |
|---|---|---|
cycles | 硬件 | CPU 周期数 |
instructions | 硬件 | 已退役(retired)指令数 |
cache-misses | 硬件 | 缓存未命中次数 |
cache-references | 硬件 | 缓存访问次数 |
branch-misses | 硬件 | 分支预测失败次数 |
branches | 硬件 | 分支指令数 |
L1-dcache-load-misses | 硬件 | L1 数据缓存加载未命中 |
LLC-load-misses | 硬件 | 末级缓存(L3)加载未命中 |
context-switches | 软件 | 上下文切换次数 |
cpu-migrations | 软件 | 进程跨 CPU 迁移次数 |
page-faults | 软件 | 缺页次数(含 minor) |
major-faults | 软件 | 主缺页(需磁盘 IO)次数 |
硬件事件由 CPU 具体型号决定(AMD / Intel / ARM 命名不完全一致),以本机 perf list 输出为准。
perf stat:计数器统计
运行一个程序并打印事件统计,判断「整体慢在哪里」。
# 基本用法
perf stat ./app
# 指定事件
perf stat -e cycles,instructions,cache-misses,cache-references ./app
# 更多默认事件(-d 追加一组)
perf stat -d ./app
# 重复运行 10 次取平均,减小噪声
perf stat -r 10 ./app
# 采样整个系统 5 秒(不指定程序时)
perf stat -a sleep 5
# 采样一个已运行进程(PID)
perf stat -p 12345 sleep 5输出解读
Performance counter stats for './app':
1,204,519,530 cycles
1,005,228,340 instructions # 0.83 insn per cycle
18,352,194 cache-misses # 12.34% of all cache refs
102,445,321 branch-misses # 2.11% of all branches
0.421988113 seconds time elapsed- insn per cycle(IPC):
instructions / cycles。理论峰值约 4(超标量),普通 CPU 密集程序 2~3,内存/IO 密集程序常 < 1。IPC 低说明 CPU 在等待(缓存 miss、依赖、分支失败)。 - cache-miss 率:
cache-misses / cache-references,一般 < 3% 较健康,> 10% 说明数据布局差、随机访问多。 - branch-miss 率:
branch-misses / branches,> 5% 说明分支难以预测(如数据依赖的 if 遍布循环)。
缺页与上下文切换(内存/调度视角)
# 观测缺页(内存分配、首次触页)
perf stat -e page-faults,minor-faults,major-faults ./app
# 观测上下文切换(锁竞争、多线程过载)
perf stat -e context-switches,cpu-migrations ./appmajor-faults 高说明频繁换页(内存不够或大量磁盘 IO);context-switches 高说明线程数远超核数或锁竞争严重。
perf record:采样与调用栈
按固定频率打断程序,把「当前调用栈 + 事件类型」写入 perf.data。
# 基本用法:默认 4000Hz 采样 cycles 事件
perf record ./app
# 用 -g 记录调用栈(热点函数回溯必需)
perf record -g ./app
# 指定采样频率(99Hz 是行业惯例,避免与定时器同步)
perf record -g -F 99 ./app
# 只跑 5 秒
perf record -g -F 99 -- ./app 2>/dev/null # 或配合 timeout
perf record -g -F 99 sleep 5 # 采样整机 5 秒(需 -a 见下)
# 采样整个系统(多进程/多线程整体看)
perf record -g -a -F 99 sleep 5
# 采样某个已运行进程 / 线程
perf record -g -p 12345 # 进程
perf record -g -t 12345 # 线程
# 指定事件:采样 cache-misses(找缓存热点)或 branch-misses
perf record -g -e cache-misses -F 99 ./app
# 输出到指定文件
perf record -g -o myperf.data ./app调用栈回溯方式(关键)
默认用**帧指针(frame pointer)**回溯,速度快但要求编译时保留帧指针(-fno-omit-frame-pointer)。若程序用 -O2 优化且没保留帧指针,栈会断掉,改用 DWARF:
# DWARF 回溯:需要 -g 编译,速度略慢但栈完整、能到源码行
perf record -g --call-graph dwarf ./app
# 或用 fp(帧指针)显式指定
perf record -g --call-graph fp ./app
频率越高时间分辨率越好,但开销和 perf.data 越大。CPU 热点分析用 99~999Hz;极短函数或高频抖动场景才用到 999Hz 以上。调频率用 -F,调「每 N 个事件采一次」用 -c N(period 采样)。
perf report:分析采样结果
# 交互式查看(默认)
perf report
# 纯文本输出(方便 grep / 存文件)
perf report --stdio
# 指定数据文件
perf report -i myperf.data
# 只看用户态(排除内核,定位自己代码)
perf report --stdio --user
# 展开调用链:caller 从调用者角度、callee 从被调者角度
perf report -g caller --stdio
perf report -g callee --stdio交互模式下常用按键:
| 按键 | 作用 |
|---|---|
Enter/+ | 展开某函数的调用链 |
e/E | 展开 / 收起全部 |
/ | 搜索函数名 |
> / < | 切换排序字段(overhead / 函数名等) |
q | 退出 |
理解 Overhead 与 Children
- Overhead(Self):该函数自身代码被采样到的比例——真正的「热代码」。
- Children:该函数自身 + 其调用的所有子函数的总占比——「总成本」,含被调用函数。
# 显示 children(总成本),默认 perf report 已是该模式
perf report --children --stdio
# 只看自身热点(self)
perf report --no-children --stdio定位时先看 Children 找「最贵的调用链」,再看 Self 锁定「具体哪几行」。
对比优化前后
perf record -g -o before.data ./app
# ...优化代码...
perf record -g -o after.data ./app
perf diff before.data after.data # 显示各函数采样占比变化(+/-)perf top:实时监控
像 top 一样实时刷新当前系统 CPU 热点,适合「跑起来边看边调」。
perf top # 实时热点
perf top -g # 带调用栈
perf top -p 12345 # 只看某进程
perf top -e cache-misses # 实时看缓存 miss 热点
perf top --call-graph dwarf # DWARF 回溯perf annotate:指令级分析
把热点函数反汇编,在每条汇编指令旁标注采样占比,定位到「具体哪条指令最慢」(如某次 load 全在等内存)。
# 先采样再标注(需要 -g 编译才能映射源码行)
perf record -g ./app
perf annotate # 交互选择热点函数,或:
perf annotate -s <函数名> # 直接指定函数,-s 指 symbol
perf annotate --stdio # 文本输出汇编中若某条 mov/load 占比畸高,通常是缓存 miss 或内存依赖,配合 perf record -e cache-misses 交叉验证。
火焰图
火焰图(Flame Graph,Brendan Gregg 发明)把采样到的调用栈叠成横条,宽度 = 采样占比,一眼看出热点与调用关系。
# 1. 采样(务必 -g)
perf record -g -F 99 ./app
# 2. 导出原始栈(perf 自带,无需额外工具)
perf script > out.perf
# 3. 折叠栈(FlameGraph 仓库脚本)
# 克隆一次即可:git clone https://github.com/brendangregg/FlameGraph.git
stackcollapse-perf.pl out.perf > out.folded
# 4. 生成 SVG
flamegraph.pl out.folded > flame.svg用浏览器打开 flame.svg,顶部最宽的「平顶」就是 CPU 热点。火焰图是聚合视图,适合找「谁在吃 CPU」;再回到 perf report/annotate 定位到行。
99Hz 不是 100 的整数倍,可避免与系统 100Hz 定时器、或程序自身的周期性事件「拍频」共振,防止采样结果被人为周期扭曲。
C++ 性能分析实战方法论
推荐按「宏观 → 微观 → 指令」三层递进:
-
宏观判断瓶颈类型(
perf stat):❯Bashperf stat -e cycles,instructions,cache-misses,cache-references,branch-misses ./app- IPC 高 → CPU 密集,去优化算法/热点函数。
- cache-miss 率高 → 优化数据结构内存布局(
std::vector连续优于链表)。 - branch-miss 率高 → 减少数据依赖分支,用查表/排序后二分。
-
锁定热点函数(
perf record -g+perf report+ 火焰图):❯Bashperf record -g -F 99 --call-graph dwarf ./app perf report --stdio --no-children | head -30 -
定位到行/指令(
perf annotate):❯Bashperf annotate -s <热点函数> -
交叉验证:换事件采样确认根因,例如怀疑缓存问题就
perf record -e cache-misses看是否命中同一处代码。
判断 CPU 瓶颈还是 IO/锁等待
- CPU 使用率高(
top显示接近 100%)→ CPU 瓶颈,用 perf 找热点函数。 - CPU 使用率低但程序慢 → 大概率 IO 等待或锁等待:
❯Bash
# 看系统调用耗时 perf trace -p <pid> 2>&1 | head # 看上下文切换(锁竞争信号) perf stat -e context-switches ./app
C++ 注意事项
- 必须
-g:否则只能看到地址,看不到函数名和源码行。 - 帧指针 vs DWARF:
-O2默认省略帧指针,若perf report里调用栈断掉,改用--call-graph dwarf(需-g编译)。 - 符号还原:perf 会自动对 C++ 符号做 demangle(把
_ZN3Foo3barEv还原为Foo::bar());若没还原,可用perf report --stdio | c++filt。 - 内联与模板:热点常显示为内联展开后的函数或模板实例(如
std::vector<int>::push_back),这是真实调用,属正常现象;perf annotate能进一步看汇编。 - 采样偏差:极短、被频繁内联的函数可能采样不到,结合
perf stat的计数器结果综合判断。 - 多线程:
perf record -a采样整机不分线程;要区分线程用perf record -t <tid>,或perf report --sort comm,dso,symbol按线程名排序。 - 与编译器优化配合:PGO(Profile-Guided Optimization)可先用 perf 采样生成 profile,再
-fprofile-use反哺编译器做定向优化。
与其他工具对比
| 工具 | 原理 | 是否改代码 | 运行时开销 | 适用场景 |
|---|---|---|---|---|
perf | PMU 采样/计数 | 否 | 低(~1-5%) | 生产环境、整体+热点+指令级分析 |
gprof | 编译插桩 | 需 -pg | 中 | 函数调用关系、调用次数 |
valgrind(callgrind) | 动态二进制翻译 | 否 | 极高(几十倍) | 精确指令级模拟、内存错误检测 |
strace | ptrace 系统调用 | 否 | 中 | 只看系统调用层面 |
VTune | PMU(Intel) | 否 | 低 | Intel 平台更丰富的硬件指标/可视化 |
perf 覆盖最广、开销最低,是 C++ 性能排查首选;需要内存泄漏/越界检测时再上 valgrind。
常见问题
Q:perf report 显示 [unknown] 或十六进制地址?
缺少调试符号或符号表被 strip。用 -g 重新编译,或对已有二进制保留符号。
Q:调用栈只有一层,看不到调用者?
帧指针被优化省略。改用 perf record -g --call-graph dwarf,或编译加 -fno-omit-frame-pointer。
Q:非 root 运行报 Permission error?
perf_event_paranoid 过严。个人机 sudo sysctl -w kernel.perf_event_paranoid=1 或加到 -1。
Q:perf stat 提示事件不存在?
事件名因 CPU 而异,先用 perf list 查本机可用事件,或用通用事件 cycles/instructions。
Q:perf.data 文件很大?
采样频率过高或 -a 全机采样。降低 -F,或只采样目标进程 -p,完成后用 perf data 可裁剪。