程序慢在哪里?不靠猜,靠数据——用 gprof 与 perf 给程序做一次全面体检
核心方法论:运筹帷幄
「善战者,先胜而后求战。优化亦然:先测得数据、再动刀改码,让每一次修改都有据可依,而不是凭感觉猜热点。运筹帷幄之中,决胜千里之外——这一章,我们学会先看全局,再落子。」
先回答最朴素的问题:性能分析(profiling)是什么?说人话,性能分析就是用工具测量程序运行时的真实行为,找出时间都花在了哪里。它回答的不是"你觉得哪里慢",而是"数据说哪里慢":哪个函数占用了最多执行时间(这叫热点(hot spot),也就是程序里最"烧时间"的那段代码)、它被谁调用、调用了多少次、CPU 周期和内存访问的效率如何。做这件事的工具叫 profiler(性能分析器),你可以把它理解成给程序做体检的医生——本章的两位主角 gprof 和 perf 都是 profiler。
为什么"不靠猜"这么重要?因为人对程序性能的直觉经常是错的:编译器优化(第 3 章讲过 -O2/-O3)会大幅改变代码的真实行为,CPU 缓存与分支预测这些硬件机制更是反直觉,标准库内部的实现你根本看不见。第 13 章讲过"链接器单遍扫描"这种隐藏规则,性能领域类似的隐藏规则只多不少。诸葛亮用兵讲究"知己知彼",优化程序也一样——先测量,后优化;先定位热点,再动手改码。这一条被称为优化第一法则,我们 15.2 会展开讲。
性能分析要回答三个层次的问题:时间花在哪(热点函数)、时间怎么被花掉的(调用关系:谁调了谁)、资源用得怎么样(CPU 周期数、指令数、缓存未命中、内存分配)。手工计时只能回答"总共花了多久",而 profiler 能把这三层都挖出来。下面先建立一个"被测程序":一个计算密集的简单程序,本章所有工具都会拿它当实验对象。
# 先感受一下"测量":time 命令给出程序运行的三种时间(Linux/macOS 都有)
time ./app
# real 0m1.094s ← 墙钟时间:从开始到结束的真实流逝时间
# user 0m1.091s ← 用户态 CPU 时间:程序自己的代码花的时间
# sys 0m0.003s ← 内核态 CPU 时间:系统调用等内核代劳的时间
// app.cpp —— 本章的被测程序:计算密集循环(巴塞尔问题的级数求和,逼近 π²/6)
#include <cstdio>
double compute(int n) {
double sum = 0.0;
for (int i = 1; i <= n; ++i) {
sum += 1.0 / i / i; // 纯 CPU 计算,热点一目了然
}
return sum;
}
int main() {
double r = compute(20000000);
std::printf("result = %.10f\n", r);
return 0;
}
把"测量"想象成斥候侦察:斥候带回的情报决定作战方案,测量的数据决定优化方案。连敌人(瓶颈)在哪都不知道就冲锋(乱改代码),是兵家大忌。本章所有命令的第一动作都是"测",而不是"改"。
计算机界有一条流传极广的告诫,出自计算机科学家高德纳(Donald Knuth)1974 年的论文:"过早的优化是万恶之源"(premature optimization is the root of all evil)。这句话常被误解成"别优化",它真正的意思是:别在不知道热点在哪的时候盲目优化——因为你十有八九在优化不重要的代码,还把代码改得更难读。正确的顺序永远是:先测量出热点,再针对热点优化。这就是先测量后优化(measure, don't guess)。
为什么热点这么集中?这里有一条经验法则叫90/10 法则:大量实测统计表明,程序大约 90% 的执行时间花在 10% 的代码上(与经济学里"80/20 法则"同理)。也就是说,一个程序通常只有一小撮函数真正决定性能——把力气花在这 10% 上,收益立竿见影;在其余 90% 的代码上精雕细琢,优化得再好也几乎感觉不到。profiler 的价值就在于此:它用数据帮你把这"10%"圈出来。
那手工打点计时(printf 记时间)够不够?不够。手工计时只能回答"整体花了多久",甚至能回答"某段花了多久",但回答不了三个关键问题:哪个函数最烧时间?它被调了多少次?时间是被谁带出来的?而且手工打点会污染被测代码、容易漏掉隐蔽路径。profiler 分两大流派解决这些问题:插桩式(instrumentation)在编译时往每个函数入口埋"记录仪",精确统计调用次数(代表:gprof);采样式(sampling)按固定频率给程序"拍快照",看快照落在哪个函数(代表:perf)。两者 15.4、15.5 会分别登场。
# 优化的标准工作流:四步循环,每一步都建立在数据之上
# 第一步:测量基线 —— 程序现在到底有多慢?
time ./app
# 第二步:定位热点 —— profiler 报告说时间花在哪?
gprof ./app gmon.out > report.txt
perf report
# 第三步:只改热点 —— 围绕热点做最小改动
# (例如:把字符串拼接改为先 reserve,或把 O(n²) 算法换成 O(n log n))
# 第四步:复测对比 —— 改动前后同一把尺子量,数据说话
time ./app # 比基线快了吗?快多少?
// 手工打点计时的朴素做法:只能测整体,测不出"哪个函数"
#include <cstdio>
#include <ctime>
int main() {
clock_t t0 = clock();
double r = compute(20000000);
clock_t t1 = clock();
std::printf("result = %.10f, %.3f s\n",
r, double(t1 - t0) / CLOCKS_PER_SEC);
return 0;
}
// 局限:只看到整段耗时;哪个函数最慢、被调几次,一概不知
// ——这正是 profiler 的用武之地。
90/10 法则的实战含义:优化是"掐尖"的艺术,不是"摊大饼"。报告里排第一的热点,优化它一天顶得上优化其余全部一个月。所以拿到 profiler 报告,先看榜首,再看调用次数异常的"小函数被狂调"——后者往往才是隐藏的深水区。
在正式引入 profiler 之前,先把"手工计时"这门基本功练扎实——它是 benchmark(基准测试,用统一标准反复测量性能)的最小单位,第 9 章 gtest 集成也可以拿它做轻量基准。C++11 标准库提供了 std::chrono 时间工具,核心是三类东西:时钟(clock)、时间点(time_point)和时长(duration)。时钟里最常用的是 steady_clock(单调时钟)——它保证时间只往前走、绝不回拨,专门用来测耗时;另一个 system_clock 是墙上时钟(显示"现在几点"用的),可能被用户或 NTP 校时跳变,测耗时不可靠。
用法三步:取开始时间点 → 跑被测代码 → 取结束时间点并求差,再用 duration_cast 把差值转成毫秒/微秒。另一个选择是 POSIX 的 clock_gettime 函数(头文件 <time.h>):它更底层、精度更高(纳秒级),CLOCK_MONOTONIC 同样是"只往前走"的单调时钟。注意在老式 Linux 系统上链接它需要加 -lrt(新版 glibc 已并入 libc,不用加)。两条路效果等价,chrono 是标准 C++ 的跨平台写法,clock_gettime 更贴近系统底层。
手工计时有三个坑:一是别用 system_clock 测耗时(理由如上);二是别只跑一次就下结论——机器负载、频率波动都是噪声,多跑几次取最短或平均才稳;三是小心编译器把测量代码"优化没了"——计算结果若没被使用,开启优化的编译器可能把整个循环删掉(死代码消除,dead code elimination),测出趋近于零的假时间。对策是给结果一个"不敢删"的去处,比如写进 volatile 变量。
// 用 std::chrono 计时:steady_clock + duration_cast
#include <chrono>
#include <cstdio>
int main() {
auto t0 = std::chrono::steady_clock::now();
double r = compute(20000000); // 被测代码
auto t1 = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0);
std::printf("result = %.10f, 耗时 %lld ms\n", r, ms.count());
return 0;
}
// 用 clock_gettime 计时:POSIX 底层接口,纳秒精度
#include <cstdio>
#include <time.h>
int main() {
struct timespec ts0, ts1;
clock_gettime(CLOCK_MONOTONIC, &ts0);
double r = compute(20000000);
clock_gettime(CLOCK_MONOTONIC, &ts1);
long ms = (ts1.tv_sec - ts0.tv_sec) * 1000
+ (ts1.tv_nsec - ts0.tv_nsec) / 1000000;
std::printf("result = %.10f, 耗时 %ld ms\n", r, ms);
return 0;
}
# 老式 Linux(glibc < 2.17)需要 -lrt:
g++ app.cpp -lrt -o app
// 防"测量被优化掉"(死代码消除):结果写进 volatile,编译器不敢删循环
double r = compute(20000000);
volatile double sink = r; // 结果"被使用",计算无法被优化掉
手工计时好比"目测敌营远近",profiler 好比"派出斥候画出完整军情图"。手工计时适合快速验证"优化有没有效",profiler 适合回答"该优化哪里"——两者配合,就是完整的先测量后优化。
gprof 是什么?它是 GNU 官方提供的性能分析工具(GNU profiler),采用插桩式(instrumentation)方案:编译时用 -pg 选项,编译器会在每个函数入口插入一小段记账代码(mcount),程序运行时这些"记录仪"精确统计每个函数被调用的次数;同时内核以固定频率采样(默认约 100 Hz,即每 0.01 秒一次),估算每个函数占用多少时间。整个流程分三步,和官方手册《GNU gprof》写的完全一致:① 用 -pg 编译链接;② 正常运行程序生成 gmon.out;③ 用 gprof 分析 gmon.out 出报告。
第二步有个关键细节:gmon.out 是程序正常退出时(从 main 返回或调用 exit)才写出来的,写在程序的工作目录里;如果用 _exit 直接退出或程序被信号打死,数据不会落盘。第三步默认读取可执行文件 a.out 和 gmon.out,所以我们通常写全:gprof ./app gmon.out。gprof 报告默认包含两部分:平面剖析(flat profile)——一张表列出每个函数占用的时间百分比、累计时间、自身时间、调用次数,按耗时从高到低排序,找热点看它就够了;调用图(call graph)——展示谁调了谁、每个函数的时间有多少是自己花的、有多少是"子函数"花的。
读平面剖析表只需盯住三列:% time(该函数占总时间百分比,榜首就是热点)、self seconds(函数自己花的秒数,不含子函数)、calls(被调用次数)。官方手册还提醒两件事:一是结果有统计误差——采样周期是 0.01 秒时,小于这个量级的时间差不可信(手册原话"Statistical Sampling Error");二是 -g 选项配合才能做行级分析,而开优化(-O2)后函数可能被内联,调用次数会失真。所以做 gprof 分析时,-O0 或 -O2 各有取舍:要精确的调用次数就用低优化,要贴近线上行为就用正常优化。
# 三步走:编译(-pg 插桩,-g 便于行级分析)→ 运行(生成 gmon.out)→ 分析
g++ -pg -g app.cpp -o app
./app
# result = 1.6449340578
ls -l gmon.out
# -rw-r--r-- 1 user user 19040 ... gmon.out ← 程序退出时自动生成的剖面数据
# 分析:gprof 可执行文件 数据文件,输出重定向到文件慢慢看
gprof ./app gmon.out > report.txt
# 只想看核心表、不要长篇解释,加 -b(brief):
gprof -b ./app gmon.out
# 平面剖析(flat profile)节选:数字因机器而异,看结构与含义
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
96.15 0.25 0.25 1 250.00 250.00 compute(int)
3.85 0.26 0.01 1 10.00 10.00 main
# 读法:compute(int) 占 96.15% 总时间、自身 0.25 秒、只被调用 1 次——热点就是它。
# 调用图(call graph)节选:谁调了谁、时间怎么传播
index % time self children called name
0.00 0.25 1/1 main [2]
[1] 96.2 0.00 0.25 1 compute(int) [1]
-----------------------------------------------
[2] 100.0 0.01 0.25 1 main [2]
0.00 0.25 1/1 compute(int) [1]
# 读法:main 调了 compute 1 次;compute 的 0.25 秒全部由 main "带出来"。
# children 巨大而 self 很小的函数是"指挥者"——时间花在它调的子函数里。
注意:gprof 是插桩 + 采样混合方案,报告有两点固有局限——① 采样间隔决定了时间分辨率:每样本 0.01 秒时,耗时低于此量级的变化测不准;② 开启 -O2/-O3 后函数可能被内联,调用次数与符号归属会"变形"。需要函数级精确数据时用低优化编译,需要贴近真实运行表现时用正常优化,二者都是合法用法,明白差异即可。
perf 是什么?它是 Linux 内核自带的性能分析工具(官方叫法:Linux profiling with performance counters),基于 CPU 内置的性能计数器(performance counter)——这是 CPU 硬件里专门记录 cycles(时钟周期)、instructions(指令数)等事件的计数器,读取开销极小,所以 perf 的采样几乎不影响被测程序。几乎每个 Linux 发行版都自带 perf(包名通常是 linux-tools 或 perf),macOS 没有 perf,这是 Linux 专属工具。perf 家族核心成员:perf stat(整体概要)、perf record / perf report(采样热点)、perf top(实时热点)、perf list(列出可用事件)。
perf stat 的职责是"跑一次命令,给出整体性能概要":默认输出 task-clock(程序占用的 CPU 时间)、context-switches(上下文切换次数)、page-faults(缺页次数)、cycles(CPU 周期)、instructions(指令数)、branches(分支指令)、branch-misses(分支预测失败)以及 time elapsed(墙钟时间)。其中最有信息量的是 IPC(instructions per cycle,每周期指令数),perf 会直接算给你看(insn per cycle):IPC 接近 1 说明 CPU 基本满载干活,IPC 很低(比如 0.1)说明 CPU 大量时间在"空转"等内存——这是典型的缓存不友好信号(15.7 会讲)。cache-misses(缓存未命中)默认不显示,加 -d 才会带出 cache-references / cache-misses 一组。
事件可以手动指定:perf stat -e cycles:u,instructions:u ./app 只统计用户态(:u)的周期与指令,:k 是内核态;机器支持哪些事件用 perf list 查看。还有一个权限细节:硬件计数器受内核参数 kernel.perf_event_paranoid 管控,某些系统上普通用户测硬件事件会被拒,加 sudo 或调低 paranoid 值即可;虚拟机里没有 PMU 支持时硬件事件可能测不到,这也是正常的。
# perf stat:跑一次程序,输出整体性能概要(数字因机器而异)
perf stat ./app
# Performance counter stats for './app':
#
# 1.094237 task-clock (msec) # 1.000 CPUs utilized
# 0 context-switches # 0.000 K/sec
# 107 page-faults # 0.098 M/sec
# 4,371,904,318 cycles # 3.995 GHz
# 6,982,110,553 instructions # 1.60 insn per cycle
# 839,414,602 branches # 767.147 M/sec
# 2,605,447 branch-misses # 0.31% of all branches
#
# 1.094680519 seconds time elapsed
#
# 读法:IPC 1.60 说明 CPU 利用率不错;branch-misses 仅 0.31%,分支预测很顺。
# 指定事件:只统计用户态周期与指令(:u = user,:k = kernel)
perf stat -e cycles:u,instructions:u ./app
# 加 -d:多出一组缓存相关计数(cache-references / cache-misses)
perf stat -d ./app
# 看看这台机器支持哪些事件(cache / branch / 软件事件等)
perf list | grep cache
# cache-references OR LLC-loads:u ...
# cache-misses OR LLC-load-misses:u ...
把 perf stat 当"开局先看军粮":它不问"哪里慢",只问"整体状态如何"。IPC 低 → 大概率在等内存(查缓存);branch-misses 高 → 分支预测被乱序数据打爆;page-faults 多 → 频繁触碰新内存页。先有整体判断,再决定用 record 深入哪一层,这就是运筹帷幄的顺序。
perf record 是采样式分析的主力:它按固定频率(默认每秒数千次,可用 -F 调整)周期性"拍快照"——每次快照记下 CPU 此刻正在执行哪个地址,也就是哪个函数。程序跑完后,采样数据写进当前目录的 perf.data 文件。采样的好处是几乎不干扰程序:不开插桩、不改编译选项,直接对线上/现成的二进制就能用。然后 perf report 读取 perf.data,按"采样点落在哪个函数"聚合成一张热点表:Overhead(该函数占总采样的百分比)、Command(进程名)、Shared Object(属于哪个库/可执行文件)、Symbol(函数符号)。榜首就是热点函数——和 gprof 的 flat profile 殊途同归,但 perf 不用重新编译。
光看函数还不够,还要知道热点是怎么被调进来的——加 -g 让 perf record 同时抓调用栈(call stack)。有了调用栈,就能画出火焰图(flame graph):把每次采样到的完整调用栈自底向上堆叠,横轴宽度代表该路径占的采样比例,越宽越热,一眼看出"热点是被哪条调用链带上来的"。配套脚本来自 Brendan Gregg 开源的 FlameGraph 项目,经典流水线:perf record -g → perf script 导出文本 → stackcollapse-perf.pl 折叠 → flamegraph.pl 生成 SVG,浏览器打开即可交互查看。
另一个高频场景是实时观察:perf top 类似系统监视器 top,但它按 CPU 采样实时显示当前最热的函数,不用跑完整个程序——适合盯一个正在运行的进程(比如线上服务)当下在忙什么。perf 家族到这里就齐了:stat 看整体,record/report 看热点,top 看实时,list 查事件。它们回答的始终是同一个问题:数据说,时间到底花在哪?
# 采样 5 秒并写 perf.data(-F 99 表示每秒采样 99 次,可自行调整)
perf record -F 99 ./app
# [ perf record: Woken up 1 times to write data ]
# [ perf record: Captured and wrote 0.019 MB perf.data (2006 samples) ]
# 分析:进入交互界面,按占比排序的热点表
perf report
# Overhead Command Shared Object Symbol
# ........ ....... ................. ..............................
# 95.21% app app [.] compute(int)
# 4.32% app app [.] main
# 0.47% app libc-2.31.so [.] __printf
#
# 读法:95.21% 的采样落在 compute(int),与 gprof 结论互相印证;[.] 用户态、[k] 内核态符号。
# 抓调用栈(-g)→ 导出文本 → 折叠 → 生成火焰图 SVG
perf record -g ./app
perf script > out.perf
stackcollapse-perf.pl out.perf > out.folded
flamegraph.pl out.folded > flamegraph.svg
# 脚本来自 Brendan Gregg 的 FlameGraph 项目;
# 浏览器打开 flamegraph.svg:横轴宽度 = 采样占比,纵轴 = 调用栈深度。
gprof 与 perf 的关系,好比"校场点兵"与"沙场观战":gprof 需要提前给部队插上旗(-pg 重编译),换来精确的调用次数;perf 不动部队就能在旁观察(直接采样现成程序),换来低干扰的真实数据。生产环境没法重编译时用 perf,开发期想要精确调用计数时用 gprof——两把尺子都备着。
拿到 profiler 报告后,热点函数通常能归入四类常见瓶颈,先认类型再对症下药:① CPU 密集——函数里是大量计算/循环,IPC 高、纯消耗算力,对策是改进算法或减少冗余计算;② 内存分配——频繁 new/delete 或字符串反复增长触发重新分配,perf stat 里 page-faults 偏高,对策是复用对象、预先 reserve;③ IO 等待——磁盘/网络读写把线程卡住,程序在"等"而不是"算",对策是异步化、批量 IO、缓存结果;④ 缓存不友好——内存访问模式跳跃,导致缓存未命中(cache miss,CPU 要的数据不在缓存里、必须去内存取)高发,IPC 因此被拉低,对策是让访问尽量连续(局部性)。perf stat 的 IPC、cache-misses、page-faults 正是识别这些类型的"化验单"。
优化优先级只有一条铁律:先算法,后微优化。O(n²) 换成 O(n log n) 是数量级的胜利,任何微优化都追不上;微优化(少一次拷贝、循环展开、内联)只是在算法定型后挤最后几个百分点。诸葛亮用兵讲究"先定谋略,再修器械"——谋略是算法,器械是微优化。编译器选项也是性价比极高的一步:-O0(不优化,调试用)、-O1/-O2(常规优化,生产默认)、-O3(更激进,含向量化),同样的代码从 -O0 换成 -O2 往往快一个数量级——所以测性能必须用与线上一致的优化等级。
C++ 特有的高频问题——不必要的拷贝——值得单独说。传参用值(std::string s)会把整个字符串复制一份;改成 const std::string& s 就只传引用。字符串用 += 反复增长时,内部缓冲区不够会触发"重新分配 + 整体拷贝",是经典的 O(n²) 陷阱(15.1 的 app.cpp 若改成拼字符串就会踩中);先 reserve 预估容量能一次性避免大量重分配。这些在 profiler 报告里往往表现为 std::__cxx11::basic_string... 这类 libstdc++ 内部符号霸榜——看到它,就明白是拷贝/分配在作祟。
最后补上 benchmark 思想:优化是否有效的唯一裁判是"同一把尺子复测"。第 9 章的 gtest 不提供基准框架,但可以拿 15.3 的 chrono 计时在 TEST 里做"冒烟基准"(只打印耗时供人工对比,不写死性能断言);正式基准可选用 Google Benchmark 库(github.com/google/benchmark),它自动处理多次运行与统计噪声。性能优化是工程习惯:每次改动后重跑 time / perf stat,把数字记进实验笔记,让优化"有账可查"。
// 缓存不友好 vs 友好:同一块数据,两种遍历顺序
#include <vector>
const int N = 4096;
std::vector<double> a(N * N); // 一维平铺模拟二维数组(行主序)
double sum_row_major() { // 按行:内存连续访问,缓存命中率高
double s = 0.0;
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
s += a[i * N + j];
return s;
}
double sum_col_major() { // 按列:每步跳 N 个元素,缓存行反复作废
double s = 0.0;
for (int j = 0; j < N; ++j)
for (int i = 0; i < N; ++i)
s += a[i * N + j];
return s;
}
// 实测(-O2):按行遍历明显更快;用 perf stat -d 对比,cache-misses 一高一低
// 避免不必要的拷贝:值传递 vs 引用;字符串先 reserve 再增长
#include <string>
// 差:按值传,每次调用整体拷贝一份字符串
void log_line(std::string s);
// 好:const 引用,只传"地址"不拷贝;只读不改就加 const
void log_line(const std::string& s);
// 字符串反复 += 前先 reserve,避免反复"重新分配 + 整体拷贝"
std::string s;
s.reserve(1000000); // 一次性预留容量
for (int i = 0; i < 100000; ++i)
s += std::to_string(i);
# 编译器优化等级对比:同样的代码,-O0 与 -O2 常常差一个数量级
g++ -O0 app.cpp -o app_O0
g++ -O2 app.cpp -o app_O2
g++ -O3 app.cpp -o app_O3
time ./app_O0
time ./app_O2
time ./app_O3
# 复测是唯一裁判:优化前后用同一把尺子(time / perf stat)对比,
# 把数字记进实验笔记——"快了多少"要有数据背书,而不是"感觉快了"。
// 第 9 章 gtest 的延伸:用 chrono 在 TEST 里做"冒烟基准"(只打印,不断言性能)
#include <gtest/gtest.h>
#include <chrono>
#include <string>
TEST(BenchmarkSmoke, BuildString) {
auto t0 = std::chrono::steady_clock::now();
std::string s;
s.reserve(1000000);
for (int i = 0; i < 100000; ++i)
s += std::to_string(i);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
std::printf("build string: %lld ms\n", ms);
EXPECT_GT(s.size(), 0u); // 断言只管正确性,耗时仅供人工对比
}
| 瓶颈类型 | 典型信号(profiler 报告里看什么) | 对策方向 |
|---|---|---|
| CPU 密集 | 热点函数 self 时间高、IPC 接近 1 | 改进算法、减少冗余计算、-O2/-O3 |
| 内存分配 | page-faults 高、std::string/vector 符号霸榜 | reserve、对象池、复用缓冲区 |
| IO 等待 | 程序总时间远大于 CPU 时间、阻塞调用多 | 异步 IO、批量读写、缓存结果 |
| 缓存不友好 | IPC 低、cache-misses 高 | 连续内存访问、调整遍历顺序、数据布局 |
把 15.4~15.7 连成一条完整的作战线:gprof/perf stat 看全局 → perf record/perf report 锁热点 → 按瓶颈类型选对策 → 优化后复测对比。性能分析不是一次性的动作,而是每次改动都要走的流程。数据在手,优化才能运筹帷幄;没有数据就动手,那是盲人摸象。
用一句话分别解释:性能分析(profiling)、热点、插桩式 profiler、采样式 profiler。再回答:90/10 法则对优化策略有什么指导意义?
四个概念对应"测量程序行为 / 最烧时间的代码 / 埋记录仪 / 拍快照";90/10 法则想想"力气花在哪 10% 上"。
性能分析:用工具测量程序运行时的真实行为,找出时间花在哪;热点:占用执行时间最多的那段代码;插桩式 profiler:编译时在函数入口埋计数代码,精确统计调用次数(代表 gprof);采样式 profiler:按固定频率给程序拍快照,看快照落在哪个函数(代表 perf)。90/10 法则说程序约 90% 的执行时间花在 10% 的代码上,因此优化应集中火力在 profiler 报告榜首的热点上——先定位那 10%,再动手。
为每个需求选一条命令:(a) 看程序整体跑了多久(三种时间);(b) 用 gprof 分析 gmon.out 并输出到 report.txt;(c) 用 -pg 编译 app.cpp 以便 gprof 分析;(d) 跑一次程序并输出整体性能概要;(e) 采样并生成 perf.data;(f) 读取 perf.data 显示热点表。
六个候选:time、g++ -pg -g、gprof、perf stat、perf record、perf report。
(a) time ./app;(b) gprof ./app gmon.out > report.txt;(c) g++ -pg -g app.cpp -o app;(d) perf stat ./app;(e) perf record ./app;(f) perf report。补充:加 -g 让 perf record 抓调用栈,加 -d 让 perf stat 多输出缓存计数。
perf stat 输出如下(示意):instructions 6.98e9、cycles 4.37e9、insn per cycle 1.60、cache-misses 512,000,000、branch-misses 0.31%。请回答:① IPC 高还是低?说明什么?② cache-misses 偏高时程序大概率属于哪类瓶颈、下一步用哪个 perf 命令确认?③ 优化顺序上应该先动算法还是先动微优化,为什么?
IPC=1.60 不算低;cache-misses 高要往内存访问模式想;优化顺序想想 15.7 的铁律。
① IPC 1.60 属于中等偏上,说明 CPU 大部分周期在有效执行指令,纯算力没有明显浪费;② cache-misses 高(5 亿次量级)说明内存访问效率差,属于"缓存不友好"类瓶颈,下一步用 perf record -g ./app 抓调用栈、看热点函数里哪些循环在跳跃访问内存,也可用 perf stat -d 对比 cache-references/cache-misses 比例确认;③ 先算法后微优化——O(n²) 换 O(n log n) 是数量级提升,微优化只能挤几个百分点,顺序反了等于在错误的战场上消耗兵力。
用本章的 app.cpp 完整做一遍:① time ./app 记录基线;② g++ -pg -g 编译,运行生成 gmon.out,用 gprof -b ./app gmon.out 看 flat profile 并指出热点函数;③ perf stat -d ./app 记录整体概要,再用 perf record -g + perf report 验证热点与 gprof 结论一致;④ 把 compute 的循环体改成 15.7 的字符串拼接版(或把 1.0 / i / i 换成更贵的计算),复测对比,验证"改动前后同一把尺子";⑤ 把每一步命令、输出关键行、结论记成实验笔记。
按 15.4 → 15.5 → 15.6 → 15.2 四步工作流的顺序走;对比时保持相同的优化等级(都 -O0 或都 -O2),否则对比无效。
① time ./app 记下 real 时间;② g++ -pg -g app.cpp -o app && ./app 生成 gmon.out(正常退出才落盘),gprof -b ./app gmon.out,榜首应为 compute(int),占 95% 以上;③ perf stat -d ./app 看计数,perf record -g ./app && perf report,Overhead 榜首同为 compute(int)——两把尺子结论一致;④ 改动后按同样优化等级重编,再跑 time 与 perf stat 对比 baseline;⑤ 笔记记录命令原文、输出关键行、前后数字——这就是"有账可查"的优化习惯。