第27章:性能分析:perf(Linux)、ETW(Windows)、火焰图生成、跨平台性能计数器
性能分析,说白了就是回答三个问题:慢在哪?为什么慢?怎么改?
我做了十几年跨平台开发,最头疼的其实不是功能实现,而是性能问题。同一个算法,在Linux上跑得飞快,到了Windows上就卡成PPT。你说是代码问题?还是系统差异?嗯,大概率是后者。
这一章,我们就来聊聊Linux和Windows两大平台上的性能分析利器——perf和ETW,以及如何用火焰图把性能瓶颈可视化,最后再谈谈跨平台性能计数器的封装思路。
27.1 Linux性能分析:perf 工具实战
perf 是Linux内核自带的性能分析工具,功能极其强大。我个人习惯把它当作“性能分析的瑞士军刀”。
27.1.1 基本用法
先看一个最简单的例子:统计程序运行时的CPU周期和指令数。
# 统计ls命令的性能事件
perf stat ls
# 输出示例:
# 1,234,567 cycles
# 2,345,678 instructions
# 0.53 insn per cycle
这里有个关键指标:IPC(每周期指令数)。IPC越高,说明CPU利用率越好。如果IPC低于0.5,那就要小心了——大概率是内存访问瓶颈或者分支预测失败。
27.1.2 采样分析
perf record 是真正的杀手锏。它通过采样,记录程序在哪些函数上花费了最多时间。
# 以99Hz的频率采样,持续10秒
perf record -F 99 -a -g -- sleep 10
# 生成报告
perf report
-g 参数会记录调用栈,这样你就能看到“谁调了谁”。-F 99 是采样频率,我个人习惯用99Hz而不是100Hz,因为99是质数,能避免与某些周期性任务产生共振。
27.2 Windows性能分析:ETW 实战
ETW(Event Tracing for Windows)是Windows上的性能分析基础设施。说实话,ETW的学习曲线比perf陡峭得多,但一旦掌握,威力巨大。
27.2.1 ETW 的基本概念
ETW有三个核心组件:
- Provider:产生事件的一方,比如内核、应用程序
- Consumer:消费事件的一方,比如分析工具
- Session:管理事件传输的会话
你可以把ETW想象成一个“系统级的事件总线”。任何组件都可以往总线上发事件,任何工具都可以从总线上收事件。
27.2.2 使用 xperf 进行采样
微软提供了 xperf 命令行工具,用法和perf类似。
# 启动采样
xperf -on PROC_THREAD+LOADER+PROFILE -stackwalk Profile
# 运行你的程序
myapp.exe
# 停止采样并保存
xperf -d trace.etl
然后用 WPA(Windows Performance Analyzer) 打开 trace.etl 文件,就能看到CPU采样结果了。
27.2.3 程序化使用ETW
如果你需要在自己的代码中集成ETW,可以使用 TraceLogging API。
#include <TraceLoggingProvider.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hMyProvider,
"MyCompany.MyComponent",
// {GUID}
(0x12345678, 0xABCD, 0xEF01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01));
void DoWork() {
TraceLoggingWrite(g_hMyProvider,
"DoWorkStart",
TraceLoggingValue(42, "InputValue"));
// ... 实际工作 ...
TraceLoggingWrite(g_hMyProvider,
"DoWorkEnd",
TraceLoggingValue(100, "Result"));
}
这样,你的程序就能产生自定义的ETW事件,和系统事件一起被WPA分析。
27.3 火焰图生成:把性能数据可视化
火焰图是Brendan Gregg发明的性能可视化方法。它把采样数据画成一张“火焰”形状的图,从下到上是调用栈,宽度代表耗时比例。
27.3.1 从perf数据生成火焰图
# 1. 采样
perf record -F 99 -a -g -- sleep 30
# 2. 生成折叠栈
perf script | ./stackcollapse-perf.pl > out.folded
# 3. 生成SVG火焰图
./flamegraph.pl out.folded > flame.svg
打开 flame.svg,你就能看到交互式的火焰图了。鼠标悬停会显示函数名和占比。
27.3.2 从ETW数据生成火焰图
Windows上生成火焰图稍微麻烦一点,但也可以做到。
# 1. 用xperf采样
xperf -on PROC_THREAD+LOADER+PROFILE -stackwalk Profile
# 运行程序
xperf -d trace.etl
# 2. 用etl2folded工具转换
etl2folded.exe trace.etl > out.folded
# 3. 生成火焰图
flamegraph.pl out.folded > flame.svg
27.4 跨平台性能计数器封装
现在问题来了:perf和ETW的API完全不同。如果你的代码需要同时在Linux和Windows上做性能分析,怎么办?
答案是:封装一个跨平台的性能计数器接口。
27.4.1 接口设计
class PerformanceCounter {
public:
virtual ~PerformanceCounter() = default;
// 开始采样
virtual void Start() = 0;
// 停止采样
virtual void Stop() = 0;
// 获取CPU时间(纳秒)
virtual uint64_t GetCPUTime() const = 0;
// 获取指令数
virtual uint64_t GetInstructions() const = 0;
// 获取缓存未命中数
virtual uint64_t GetCacheMisses() const = 0;
// 获取上下文切换次数
virtual uint64_t GetContextSwitches() const = 0;
};
27.4.2 Linux实现
Linux上,我们可以用 perf_event_open 系统调用来读取硬件计数器。
class LinuxPerformanceCounter : public PerformanceCounter {
int fd_cpu;
int fd_instructions;
int fd_cache;
public:
void Start() override {
// 打开perf事件
struct perf_event_attr pe;
memset(&pe, 0, sizeof(pe));
pe.size = sizeof(pe);
pe.disabled = 1;
pe.exclude_kernel = 1;
pe.exclude_hv = 1;
// CPU周期
pe.type = PERF_TYPE_HARDWARE;
pe.config = PERF_COUNT_HW_CPU_CYCLES;
fd_cpu = perf_event_open(&pe, 0, -1, -1, 0);
// 指令数
pe.config = PERF_COUNT_HW_INSTRUCTIONS;
fd_instructions = perf_event_open(&pe, 0, -1, -1, 0);
// 缓存未命中
pe.config = PERF_COUNT_HW_CACHE_MISSES;
fd_cache = perf_event_open(&pe, 0, -1, -1, 0);
// 启用计数
ioctl(fd_cpu, PERF_EVENT_IOC_ENABLE, 0);
ioctl(fd_instructions, PERF_EVENT_IOC_ENABLE, 0);
ioctl(fd_cache, PERF_EVENT_IOC_ENABLE, 0);
}
uint64_t GetCPUTime() const override {
uint64_t val;
read(fd_cpu, &val, sizeof(val));
return val;
}
// ... 其他实现
};
27.4.3 Windows实现
Windows上,我们可以用 QueryPerformanceCounter 和 GetProcessTimes 来获取性能数据。
class WindowsPerformanceCounter : public PerformanceCounter {
LARGE_INTEGER start_time;
LARGE_INTEGER freq;
HANDLE hProcess;
public:
WindowsPerformanceCounter() {
QueryPerformanceFrequency(&freq);
hProcess = GetCurrentProcess();
}
void Start() override {
QueryPerformanceCounter(&start_time);
}
uint64_t GetCPUTime() const override {
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
return (now.QuadPart - start_time.QuadPart) * 1000000000 / freq.QuadPart;
}
uint64_t GetContextSwitches() const override {
// 使用GetProcessTimes获取上下文切换次数
FILETIME create, exit, kernel, user;
GetProcessTimes(hProcess, &create, &exit, &kernel, &user);
// 注意:这里需要额外调用GetProcessIoCounters等API
return 0; // 简化处理
}
};
Vista 及以上版本的 QueryProcessCycleTime 或者 ETW 的 PMU 事件。如果你的目标平台是Windows 10+,建议直接使用ETW的 PROFILE 事件。
27.5 知识体系图
下面这张图展示了本章的核心知识结构:
27.6 实战建议
最后,分享几个我在项目中积累的经验:
- 先做采样,再做统计:先用perf record或ETW采样,找到热点函数,再用perf stat精确统计。
- 火焰图要对比着看:优化前和优化后各生成一张火焰图,对比差异。我习惯把两张图叠在一起,用颜色区分。
- 跨平台封装不要过度:性能计数器是平台相关的,强行统一所有接口反而会丢失平台特性。我建议只封装最常用的几个指标(CPU时间、指令数、缓存未命中),其他用平台特定的API。
- 注意采样开销:perf和ETW本身都有开销。在低端设备上,采样频率过高会导致程序变慢,影响测试结果。
好了,这一章的内容就到这里。性能分析是个实践性很强的工作,光看文档是不够的。我建议你找个实际项目,用perf或ETW跑一遍,生成火焰图,然后尝试优化。只有亲手做过,才能真正理解这些工具的价值。