3、Perf实战:CPU周期与指令计数
好,咱们进入正题。Perf stat 这个工具,说白了就是给你程序做「体检」的。它能告诉你程序跑了多少指令、花了多少CPU周期。这两个数字一除,就是CPI——每指令周期数。这个指标,我做了这么多年ADAS,几乎天天都要看。
3.1 perf stat 的基本用法
先来个最简单的。假设你写了个矩阵乘法程序,叫 mat_mul。想看看它跑得怎么样?一行命令搞定:
perf stat ./mat_mul
输出大概长这样:
Performance counter stats for './mat_mul':
12345678 cycles # 3.2 GHz
9876543 instructions # 0.80 insn per cycle
2345678 branch-misses # 5.6% of all branches
0.003456789 seconds time elapsed
看到没?关键信息就几个:cycles(周期数)、instructions(指令数)、insn per cycle(IPC,每周期指令数)。IPC 和 CPI 是倒数关系,IPC=0.8 意味着 CPI=1.25。我个人习惯看 IPC,因为数字越大越好,直观。
3.2 理解CPI:为什么它这么重要?
CPI 是啥?就是执行一条指令平均需要多少个CPU周期。理想情况下,现代CPU可以做到 CPI=1,甚至更低(比如0.5)。但现实往往很骨感。
我在项目中遇到过一件事。有个ADAS的感知算法,跑在嵌入式平台上,帧率死活上不去。我一看 perf stat 结果,CPI 高达 3.5!这意味着CPU大部分时间都在等数据,而不是真正干活。后来发现是内存访问模式太差,cache miss 太多。
为什么会这样?你想想看,CPU 从 L1 cache 取数据只要几个周期,但从 DDR 内存取数据要几百个周期。如果程序频繁 cache miss,CPI 自然就上去了。
3.3 案例分析:矩阵乘法优化
咱们拿矩阵乘法开刀。这是ADAS里最常见的运算之一——图像变换、特征提取、神经网络推理,底层全是矩阵乘法。
先写个最朴素的版本:
// naive_mat_mul.c
void mat_mul(float *A, float *B, float *C, int N) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
float sum = 0.0f;
for (int k = 0; k < N; k++) {
sum += A[i * N + k] * B[k * N + j];
}
C[i * N + j] = sum;
}
}
}
用 perf stat 跑一下(N=1024):
perf stat ./naive_mat_mul
结果:
5,234,567,890 cycles
8,765,432,100 instructions
1.67 IPC
0.60 CPI
CPI=0.6?看起来不错?别急,这是理想情况。实际上,这个版本 cache 利用率极差。我们看看 cache miss:
perf stat -e cache-misses,cache-references ./naive_mat_mul
结果:
1,234,567,890 cache-references
987,654,321 cache-misses # 80% miss rate
80% 的 cache miss!这意味着大部分时间CPU都在等内存。虽然指令级CPI看着低,但实际执行时间很长。
嗯,这里要注意。perf stat 给出的 cycles 是CPU实际运行的周期数,但其中包含了 stall 周期(等待内存、等待分支等)。所以 CPI 低不代表程序快,要看绝对时间。
3.4 优化:循环分块(Loop Tiling)
怎么优化?核心思路是提高数据局部性。让数据尽量留在 cache 里,别老往内存跑。
我常用的手法是循环分块。把大矩阵切成小块,让每个小块能塞进 L1 cache:
// tiled_mat_mul.c
void mat_mul_tiled(float *A, float *B, float *C, int N) {
int BLOCK = 64; // 根据L1 cache大小调整
for (int i = 0; i < N; i += BLOCK) {
for (int j = 0; j < N; j += BLOCK) {
for (int k = 0; k < N; k += BLOCK) {
// 处理一个 BLOCK x BLOCK 的小块
for (int ii = i; ii < i + BLOCK; ii++) {
for (int jj = j; jj < j + BLOCK; jj++) {
float sum = C[ii * N + jj];
for (int kk = k; kk < k + BLOCK; kk++) {
sum += A[ii * N + kk] * B[kk * N + jj];
}
C[ii * N + jj] = sum;
}
}
}
}
}
}
再跑一次 perf stat:
perf stat ./tiled_mat_mul
结果:
2,100,000,000 cycles
8,800,000,000 instructions
4.19 IPC
0.24 CPI
cache miss 率从 80% 降到了 15% 左右。执行时间缩短了 60% 以上。
3.5 用 perf stat 对比优化效果
优化前后对比,我习惯用这个命令:
perf stat -r 5 -e cycles,instructions,cache-misses,cache-references ./tiled_mat_mul
-r 5 表示跑5次取平均,避免偶然误差。输出结果一目了然:
| 指标 | 朴素版本 | 分块版本 | 提升 |
|---|---|---|---|
| Cycles | 5.23e9 | 2.10e9 | 2.5x |
| IPC | 1.67 | 4.19 | 2.5x |
| Cache miss rate | 80% | 15% | 5.3x |
| 执行时间 | 1.63s | 0.66s | 2.5x |
你看,IPC 从 1.67 涨到 4.19,CPI 从 0.6 降到 0.24。但真正重要的是执行时间缩短了 2.5 倍。这就是 perf stat 的价值——帮你找到瓶颈,验证优化效果。
3.6 小结
perf stat 是性能分析的「第一把刀」。它能快速告诉你:
- 程序跑了多少指令、花了多少周期
- CPI/IPC 是否健康
- cache 效率如何
记住:优化不是玄学,是数据驱动的。每次改代码,跑一遍 perf stat,看数字说话。下一节咱们会深入 perf record,看看到底是哪段代码在拖后腿。