📋 核心要点
  • 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 热点。

安装与权限配置

安装

Bash
# Debian / Ubuntu
sudo apt install linux-tools-common linux-tools-generic linux-tools-$(uname -r)

# CentOS / RHEL / Fedora
sudo yum install perf

验证:

Bash
perf --version          # 版本
perf test              # 自检

权限

非 root 用户采样受 /proc/sys/kernel/perf_event_paranoid 控制,数值越大限制越严:

值权限含义(对非特权用户)
-1允许使用几乎所有事件(含内核事件),忽略 mlock 限制
0禁止访问 tracepoint 原始数据
1禁止 CPU 级(PMU 硬件)事件,仅允许软件事件
2禁止内核态采样(只能采样用户态)
3禁止所有事件(Ubuntu 22.04 起默认,非 root 几乎用不了)
Bash
# 查看当前值
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++)

采样要看到函数名和调用栈,需满足:

Bash
# 必须有调试符号(-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:列出可用事件

Bash
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:计数器统计

运行一个程序并打印事件统计,判断「整体慢在哪里」。

Bash
# 基本用法
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

输出解读

Text
 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 遍布循环)。

缺页与上下文切换(内存/调度视角)

Bash
# 观测缺页(内存分配、首次触页)
perf stat -e page-faults,minor-faults,major-faults ./app

# 观测上下文切换(锁竞争、多线程过载)
perf stat -e context-switches,cpu-migrations ./app

major-faults 高说明频繁换页(内存不够或大量磁盘 IO);context-switches 高说明线程数远超核数或锁竞争严重。

perf record:采样与调用栈

按固定频率打断程序,把「当前调用栈 + 事件类型」写入 perf.data。

Bash
# 基本用法:默认 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:

Bash
# 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:分析采样结果

Bash
# 交互式查看(默认)
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:该函数自身 + 其调用的所有子函数的总占比——「总成本」,含被调用函数。
Bash
# 显示 children(总成本),默认 perf report 已是该模式
perf report --children --stdio

# 只看自身热点(self)
perf report --no-children --stdio

定位时先看 Children 找「最贵的调用链」,再看 Self 锁定「具体哪几行」。

对比优化前后

Bash
perf record -g -o before.data ./app
# ...优化代码...
perf record -g -o after.data  ./app
perf diff before.data after.data   # 显示各函数采样占比变化(+/-)

perf top:实时监控

像 top 一样实时刷新当前系统 CPU 热点,适合「跑起来边看边调」。

Bash
perf top                      # 实时热点
perf top -g                   # 带调用栈
perf top -p 12345             # 只看某进程
perf top -e cache-misses      # 实时看缓存 miss 热点
perf top --call-graph dwarf   # DWARF 回溯

perf annotate:指令级分析

把热点函数反汇编,在每条汇编指令旁标注采样占比,定位到「具体哪条指令最慢」(如某次 load 全在等内存)。

Bash
# 先采样再标注(需要 -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 发明)把采样到的调用栈叠成横条,宽度 = 采样占比,一眼看出热点与调用关系。

Bash
# 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

99Hz 不是 100 的整数倍,可避免与系统 100Hz 定时器、或程序自身的周期性事件「拍频」共振,防止采样结果被人为周期扭曲。

C++ 性能分析实战方法论

推荐按「宏观 → 微观 → 指令」三层递进:

  1. 宏观判断瓶颈类型(perf stat):

    Bash
    perf stat -e cycles,instructions,cache-misses,cache-references,branch-misses ./app
    • IPC 高 → CPU 密集,去优化算法/热点函数。
    • cache-miss 率高 → 优化数据结构内存布局(std::vector 连续优于链表)。
    • branch-miss 率高 → 减少数据依赖分支,用查表/排序后二分。
  2. 锁定热点函数(perf record -g + perf report + 火焰图):

    Bash
    perf record -g -F 99 --call-graph dwarf ./app
    perf report --stdio --no-children | head -30
  3. 定位到行/指令(perf annotate):

    Bash
    perf annotate -s <热点函数>
  4. 交叉验证:换事件采样确认根因,例如怀疑缓存问题就 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 反哺编译器做定向优化。

与其他工具对比

工具原理是否改代码运行时开销适用场景
perfPMU 采样/计数否低(~1-5%)生产环境、整体+热点+指令级分析
gprof编译插桩需 -pg中函数调用关系、调用次数
valgrind(callgrind)动态二进制翻译否极高(几十倍)精确指令级模拟、内存错误检测
straceptrace 系统调用否中只看系统调用层面
VTunePMU(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 可裁剪。

来源