4. perf stat
perf stat 是 perf 工具集中最基础也最常用的子命令,用于在程序运行期间收集并汇总关键硬件性能事件计数。它不进行采样或追踪,而是通过内核的 性能监控单元(PMU) 驱动,在程序生命周期内精确统计 CPU 周期、指令数、缓存引用与缺失、分支预测成功与失败等底层指标。这些计数是理解程序微架构效率、定位瓶颈的第一步。
核心价值: 与采样工具不同,
perf stat 提供的是 精确的全局计数,而非统计近似值。它适合快速评估程序整体性能特征,验证优化假设,或作为深入分析的起点。
4.1 基本用法与输出解读
最简单的用法是直接在待分析命令前加上 perf stat:
# 统计 ls 命令的性能事件
perf stat ls
# 统计一个运行时间较长的计算程序
perf stat ./my_heavy_app
执行后,perf stat 会输出类似下面的表格,包含事件名称、计数、单位以及时间信息:
| 事件名称 | 计数 | 单位 | 说明 |
|---|---|---|---|
| task-clock | 1.23 | msec | 任务实际占用的 CPU 时间 |
| context-switches | 5 | # | 上下文切换次数 |
| cpu-migrations | 0 | # | CPU 迁移次数 |
| page-faults | 128 | # | 缺页异常次数 |
| cycles | 3,456,789 | # | 消耗的 CPU 周期数 |
| instructions | 2,100,000 | # | 执行的指令数 |
| branches | 450,000 | # | 分支指令数 |
| branch-misses | 12,000 | # | 分支预测失败次数 |
| seconds time elapsed | 0.012 | sec | 程序实际运行墙钟时间 |
关键指标速算:
- CPI(Cycles Per Instruction) = cycles / instructions。理想值接近 1.0,值越大说明指令执行效率越低。
- 分支预测命中率 = (branches - branch-misses) / branches × 100%。现代 CPU 通常 >95%,低于 90% 需关注。
4.2 硬件事件选择与自定义
默认事件集覆盖了基本指标,但实际分析中常需要针对特定微架构瓶颈选择更细粒度的事件。使用 -e 参数可以指定一个或多个事件:
# 仅统计缓存相关事件
perf stat -e cache-references,cache-misses ./app
# 统计多个事件,用逗号分隔
perf stat -e cycles,instructions,branches,branch-misses,L1-dcache-loads,L1-dcache-load-misses ./app
不同 CPU 微架构支持的硬件事件名称可能不同。使用 perf list 可以查看当前系统支持的所有事件:
# 列出所有硬件事件
perf list hardware
# 列出缓存相关事件
perf list cache
# 列出所有可用事件(数量庞大)
perf list
| 事件类别 | 常用事件名 | 分析目标 |
|---|---|---|
| 核心流水线 | cycles, instructions, stalled-cycles-frontend, stalled-cycles-backend | 判断瓶颈在前端取指还是后端执行 |
| 分支预测 | branches, branch-misses | 评估分支预测器效率 |
| L1 数据缓存 | L1-dcache-loads, L1-dcache-load-misses, L1-dcache-stores | 数据访问局部性分析 |
| LLC(最后一级缓存) | LLC-loads, LLC-load-misses, LLC-stores, LLC-store-misses | 内存访问瓶颈,跨核数据共享 |
| TLB | dTLB-load-misses, iTLB-load-misses | 页表遍历开销 |
4.3 缓存层次结构与命中率分析
现代 CPU 采用多级缓存架构,perf stat 可以分别统计各级缓存的访问与缺失情况。下图展示了典型的三级缓存结构及 perf 事件对应关系:
注意: 缓存缺失率(miss rate)并不直接等于性能损失。一次 L1 缺失可能仅延迟十几个周期,而一次 LLC 缺失导致的内存访问可能延迟数百个周期。分析时应结合 绝对缺失次数 和 CPI 综合判断。
4.4 分支预测与流水线效率
分支预测是现代 CPU 维持高指令吞吐量的关键。当分支预测失败时,流水线必须冲刷并重新取指,造成显著的性能惩罚。perf stat 可以量化这一开销。
以下示例展示如何分析一个包含大量条件分支的程序:
# 统计分支相关事件
perf stat -e branches,branch-misses,branch-loads,branch-load-misses ./branch_heavy_app
# 输出示例
# 45,678,912 branches
# 2,345,678 branch-misses # 5.13% of all branches
# 12,345,678 branch-loads
# 1,234,567 branch-load-misses # 10.00% of all branch loads
分支预测失败率超过 5% 通常意味着代码中存在难以预测的模式(如二分查找、随机数据驱动的 switch-case)。优化方向包括:
- 使用分支友好的数据结构:如用查找表替代条件判断。
- 消除循环内的分支:循环展开、函数内联。
- 使用
__builtin_expect:向编译器提示分支方向。
4.5 多事件组合与比率计算
perf stat 支持在一条命令中同时统计多个事件,并自动计算比率。使用 -r N 可以重复运行 N 次并取平均值,提高数据稳定性:
# 重复运行 3 次,统计指令数与周期数,自动计算 IPC
perf stat -r 3 -e cycles,instructions ./compute_app
# 输出示例(每次运行单独显示,最后汇总平均值)
# Performance counter stats for './compute_app' (3 runs):
#
# 1,234,567,890 cycles ( +- 0.23% )
# 2,345,678,901 instructions # 1.90 insn per cycle ( +- 0.15% )
#
# 0.1234 +- 0.0005 seconds time elapsed ( +- 0.41% )
实用比率组合:
- IPC(Instructions Per Cycle) = instructions / cycles。越高越好,通常 >1.5 表示良好。
- 缓存缺失率 = cache-misses / cache-references。
- 分支预测失败率 = branch-misses / branches。
- TLB 缺失率 = dTLB-load-misses / dTLB-loads。
4.6 实战案例:矩阵乘法性能分析
以下通过一个简单的矩阵乘法程序,演示如何使用 perf stat 定位缓存问题:
// matrix_mul.c
#define N 1024
double A[N][N], B[N][N], C[N][N];
void matmul_naive() {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
for (int k = 0; k < N; k++)
C[i][j] += A[i][k] * B[k][j];
}
int main() {
matmul_naive();
return 0;
}
# 编译并分析
gcc -O2 -o matmul matrix_mul.c
perf stat -e cycles,instructions,L1-dcache-load-misses,LLC-load-misses ./matmul
输出分析:
| 事件 | 计数 | 比率 | 诊断 |
|---|---|---|---|
| cycles | 12,345,678,900 | — | 总周期数较高 |
| instructions | 8,765,432,100 | IPC = 0.71 | 远低于理想值,存在瓶颈 |
| L1-dcache-load-misses | 1,234,567,890 | 缺失率 ≈ 14% | 数据访问局部性差 |
| LLC-load-misses | 456,789,012 | 缺失率 ≈ 37% | 大量内存访问未命中 LLC |
结论:缓存缺失是主要瓶颈。优化方向为循环分块(tiling)或交换循环顺序以提高空间局部性。
优化验证: 应用循环分块优化后,再次运行
perf stat,观察 L1 缺失率和 LLC 缺失率是否显著下降,IPC 是否提升。这是典型的“测量-优化-再测量”闭环。
4.7 常见陷阱与注意事项
- 事件复用(Event Multiplexing):当同时监控的事件数超过 PMU 硬件计数器数量时,内核会分时复用计数器,导致计数为统计近似值。输出中会显示
(scaled from xx%)提示。 - 超线程影响:同一物理核上的两个逻辑核共享部分缓存和执行单元,事件计数可能互相干扰。分析时建议绑定到特定 CPU 或禁用超线程。
- 内核 vs 用户空间:默认统计用户态和内核态事件。使用
:u和:k后缀可以区分,例如cycles:u仅统计用户态周期。 - 短生命周期程序:对于运行时间极短的程序(如
ls),事件计数可能过小,统计意义不大。使用-r重复运行或选择更长时间的程序。
# 仅统计用户态事件,避免内核干扰
perf stat -e cycles:u,instructions:u,branches:u ./app
# 绑定到 CPU 0,减少迁移影响
perf stat -C 0 ./app