第27章:性能分析:perf(Linux)、ETW(Windows)、火焰图生成、跨平台性能计数器

性能分析,说白了就是回答三个问题:慢在哪?为什么慢?怎么改?

我做了十几年跨平台开发,最头疼的其实不是功能实现,而是性能问题。同一个算法,在Linux上跑得飞快,到了Windows上就卡成PPT。你说是代码问题?还是系统差异?嗯,大概率是后者。

这一章,我们就来聊聊Linux和Windows两大平台上的性能分析利器——perfETW,以及如何用火焰图把性能瓶颈可视化,最后再谈谈跨平台性能计数器的封装思路。

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,那就要小心了——大概率是内存访问瓶颈或者分支预测失败。

我的经验: 我在项目中遇到过一段图像处理代码,IPC只有0.3。一开始以为是算法问题,后来用perf stat一看,发现cache miss率高达40%。改成按行访问内存后,IPC直接飙到1.2,性能提升了3倍。

27.1.2 采样分析

perf record 是真正的杀手锏。它通过采样,记录程序在哪些函数上花费了最多时间。

# 以99Hz的频率采样,持续10秒
perf record -F 99 -a -g -- sleep 10

# 生成报告
perf report

-g 参数会记录调用栈,这样你就能看到“谁调了谁”。-F 99 是采样频率,我个人习惯用99Hz而不是100Hz,因为99是质数,能避免与某些周期性任务产生共振。

注意: 采样频率不要太高。我曾经在服务器上设了1000Hz,结果perf本身占用了5%的CPU,反而影响了测试结果。99Hz是个安全的选择。

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采样结果了。

关键点: ETW的采样数据比perf更丰富。它不仅能记录CPU时间,还能记录磁盘I/O、网络、内存分配、上下文切换等。我曾在一次性能排查中,通过ETW发现了一个“磁盘队列长度”异常,最终定位到是杀毒软件在扫描临时文件。

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分析。

避坑指南: 我曾经在ETW Provider的GUID上吃过亏。两个不同的组件如果用了相同的GUID,事件会互相干扰。所以,一定要为每个组件生成唯一的GUID。

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
我的习惯: 我通常会在火焰图上找“平顶山”——就是那些宽度很大、顶部平坦的函数。这些函数往往是性能瓶颈。如果看到某个函数占了30%以上的宽度,那它大概率就是你要优化的目标。

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上,我们可以用 QueryPerformanceCounterGetProcessTimes 来获取性能数据。

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; // 简化处理
    }
};
注意: Windows上获取指令数和缓存未命中数比较麻烦,需要用到 Vista 及以上版本的 QueryProcessCycleTime 或者 ETWPMU 事件。如果你的目标平台是Windows 10+,建议直接使用ETW的 PROFILE 事件。

27.5 知识体系图

下面这张图展示了本章的核心知识结构:

跨平台性能分析知识体系 性能分析 Linux: perf perf stat:统计事件 perf record:采样分析 perf_event_open:编程接口 Windows: ETW xperf:命令行工具 WPA:可视化分析 TraceLogging:编程接口 火焰图生成 perf script → stackcollapse etl2folded → flamegraph.pl 输出:交互式SVG 跨平台封装 抽象接口:PerformanceCounter Linux:perf_event_open Windows:QueryPerformanceCounter

27.6 实战建议

最后,分享几个我在项目中积累的经验:

  • 先做采样,再做统计:先用perf record或ETW采样,找到热点函数,再用perf stat精确统计。
  • 火焰图要对比着看:优化前和优化后各生成一张火焰图,对比差异。我习惯把两张图叠在一起,用颜色区分。
  • 跨平台封装不要过度:性能计数器是平台相关的,强行统一所有接口反而会丢失平台特性。我建议只封装最常用的几个指标(CPU时间、指令数、缓存未命中),其他用平台特定的API。
  • 注意采样开销:perf和ETW本身都有开销。在低端设备上,采样频率过高会导致程序变慢,影响测试结果。
避坑指南: 我曾经在ARM Linux上使用perf,发现某些硬件计数器不支持。后来查了文档才知道,ARM的PMU(性能监控单元)和x86完全不同。所以,跨平台性能分析一定要在目标硬件上测试。

好了,这一章的内容就到这里。性能分析是个实践性很强的工作,光看文档是不够的。我建议你找个实际项目,用perf或ETW跑一遍,生成火焰图,然后尝试优化。只有亲手做过,才能真正理解这些工具的价值。