强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

C/C++ Linux 开发教程(GCC + CMake) / 性能分析与优化(perf/gprof/编译器优化)

性能分析与优化(perf/gprof/编译器优化)

“不要猜,要量度。” 性能优化的第一步永远是分析瓶颈在哪里。本文介绍 gprof、perf、火焰图以及编译器级别的优化手段。

gprof 性能分析

gprof 基本流程

# 步骤 1: 编译时启用 profiling
gcc -pg -O2 -g main.c utils.c -o app

# 步骤 2: 运行程序(生成 gmon.out)
./app

# 步骤 3: 分析结果
gprof app gmon.out > profile.txt

# 步骤 4: 查看报告
less profile.txt

gprof 报告解读

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 45.20     0.56     0.56   100000     0.01     0.01  sort_compare
 28.30     0.91     0.35    50000     0.01     0.02  process_record
 15.10     1.10     0.19        1   190.00   890.00  load_data
  8.40     1.21     0.11    50000     0.00     0.00  hash_lookup
  3.00     1.24     0.04        1    40.00    40.00  init_config

 %         函数执行时间占总时间的百分比
 cumulative  累计时间(包含子函数)
 self        函数自身时间(不含子函数)
 calls       调用次数
 ms/call     每次调用平均耗时

gprof 与 CMake 集成

option(ENABLE_PROFILING "Enable gprof profiling" OFF)

if(ENABLE_PROFILING)
    add_compile_options(-pg)
    add_link_options(-pg)
endif()
cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_PROFILING=ON
cmake --build build
cd build && ./app
gprof ./app gmon.out > profile.txt

gprof 局限性

局限说明
采样频率默认 10ms,短函数可能不准
I/O 函数不计入分析(阻塞时间被忽略)
内联函数统计可能不准确
多线程只统计主线程
信号处理信号处理函数不在统计范围内

perf 性能分析

perf 基本使用

# 安装 perf
sudo apt install linux-tools-$(uname -r)

# 基本统计
perf stat ./app

# 输出示例:
#  Performance counter stats for './app':
#
#          1,234.56 msec  task-clock                #    0.985 CPUs utilized
#               12        context-switches          #    9.721 /sec
#                2        cpu-migrations            #    1.620 /sec
#            8,765        page-faults               #    7.099 K/sec
#    4,567,890,123        cycles                    #    3.700 GHz
#    2,345,678,901        instructions              #    0.51  insn per cycle
#      345,678,901        branches                  #  280.009 M/sec
#        1,234,567        branch-misses             #    0.36% of branches
#
#       1.253210123 seconds time elapsed

perf record + report

# 记录性能数据
perf record -g ./app

# 生成火焰图数据(使用调用图)
perf record -g --call-graph dwarf ./app

# 查看报告
perf report

# 交互式查看
perf report --stdio

# 输出为文本
perf report --stdio --sort comm,dso,sym > perf_report.txt

perf 单项统计

# 缓存命中率
perf stat -e cache-references,cache-misses ./app

# 分支预测
perf stat -e branches,branch-misses ./app

# 指令数
perf stat -e instructions,cycles ./app

# IPC (Instructions Per Cycle)
perf stat -e instructions,cycles -a sleep 5

# 页面错误
perf stat -e page-faults,minor-faults,major-faults ./app

# 上下文切换
perf stat -e context-switches,cpu-migrations ./app

perf 实时监控

# 实时监控热点函数
perf top

# 按 PID 监控
perf top -p <PID>

# 指定事件
perf top -e cache-misses -p <PID>

火焰图生成

使用 FlameGraph 工具

# 安装 FlameGraph
git clone https://github.com/brendangregg/FlameGraph.git
export PATH=$PATH:$PWD/FlameGraph

# 方法 1: perf 数据生成火焰图
perf record -g --call-graph dwarf ./app
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg

# 方法 2: 使用 perf 的简化命令
perf record -g ./app
perf script | stackcollapse-perf.pl > out.stacks
flamegraph.pl --title "My App" --width 1200 out.stacks > flamegraph.svg

# 打开查看
xdg-open flamegraph.svg

火焰图解读

┌──────────────────────────────────────────────────┐
│                  main (100%)                      │
├──────────┬───────────────────┬───────────────────┤
│ process  │    sort_data      │    output_result   │
│  (30%)   │     (55%)         │      (15%)         │
├──────────┼───────────┬───────┼───────────────────┤
│ hash_    │ quick_    │merge_ │   write_file      │
│ lookup   │ sort(40%) │sort   │     (15%)         │
│ (15%)    │           │(15%)  │                   │
└──────────┴───────────┴───────┴───────────────────┘

# 宽度 = 该函数在采样中出现的比例
# 高度 = 调用栈深度
# 颜色 = 无特殊含义(仅区分不同函数)

差分火焰图(Diff Flamegraph)

# 对比两次采样
# 采样优化前
perf record -g -o perf_before.data ./app_before
perf script -i perf_before.data | stackcollapse-perf.pl > before.stacks

# 采样优化后
perf record -g -o perf_after.data ./app_after
perf script -i perf_after.data | stackcollapse-perf.pl > after.stacks

# 生成差分火焰图
difffolded.pl before.stacks after.stacks | flamegraph.pl > diff.svg

编译器优化选项详解

优化级别详解

选项说明代码大小运行速度编译时间
-O0无优化最大最慢最快
-O1基础优化较小较快
-O2标准优化较小
-O3激进优化可能变大最快
-Os大小优化最小较快
-Og调试优化较大较快
-Ofast极速优化较大最快

-O2 与 -O3 的区别

# -O2 包含的优化(安全、常用)
# -fthread-jumps
# -falign-functions
# -falign-loops
# -fcaller-saves
# -fcrossjumping
# -fcse-follow-jumps
# -fdelete-null-pointer-checks
# -fdevirtualize
# -fgcse
# -fhoist-adjacent-loads

# -O3 额外包含(可能增加代码大小)
# -ftree-loop-distribute-patterns
# -fpredictive-commoning
# -ftree-vectorize          ← 自动向量化
# -fgcse-after-reload
# -ftree-slp-vectorize
# -fvect-cost-model
# 对比优化效果
gcc -O0 benchmark.c -o bench_O0
gcc -O2 benchmark.c -o bench_O2
gcc -O3 benchmark.c -o bench_O3

time ./bench_O0
time ./bench_O2
time ./bench_O3

# 查看编译器执行了哪些优化
gcc -O3 -fopt-info-vec-all benchmark.c -o benchmark 2>&1 | head

-Ofast 的风险

# -Ofast 包含 -ffast-math,可能导致:
# 1. 不遵守 IEEE 754 浮点标准
# 2. 不处理 NaN 和 Inf
# 3. 假设浮点运算满足结合律和分配律
# 4. 精度可能降低

# ❌ 不要在科学计算中使用 -Ofast
# ✅ 在游戏/媒体等允许精度损失的场景可考虑

# 只启用部分 fast-math
gcc -O3 -fno-math-errno -fno-trapping-math benchmark.c -o benchmark

LTO 链接时优化

什么是 LTO

LTO(Link-Time Optimization)在链接阶段对所有编译单元进行全局优化,可以跨文件内联、消除未使用的代码等。

# GCC LTO
gcc -O2 -flto main.c utils.c -o app

# 分步使用 LTO
gcc -O2 -flto -c main.c -o main.o      # 生成 LTO 中间码
gcc -O2 -flto -c utils.c -o utils.o    # 生成 LTO 中间码
gcc -O2 -flto main.o utils.o -o app     # 链接时全局优化

# 查看 LTO 优化信息
gcc -O2 -flto -flto-report main.c utils.c -o app 2> lto_report.txt

LTO 与 CMake

option(ENABLE_LTO "Enable Link-Time Optimization" OFF)

if(ENABLE_LTO)
    set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()

# 或者对单个目标启用
set_target_properties(mylib PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_LTO=ON

ThinLTO(Clang 专用)

# ThinLTO: 更快的 LTO,增量编译支持更好
clang -O2 -flto=thin main.c utils.c -o app

# 与完整 LTO 对比:
# 完整 LTO: 优化效果更好,但编译很慢
# ThinLTO: 优化效果接近,但编译快 2-5 倍

LTO 效果对比

指标无 LTO有 LTO
可执行文件大小基准-5% ~ -15%
运行速度基准+5% ~ +20%
编译时间基准+30% ~ +100%
链接时间基准+100% ~ +500%

PGO 引导优化

PGO 工作流程

1. 插桩编译 → 2. 运行收集数据 → 3. 使用数据重新编译

GCC PGO 实践

# 步骤 1: 插桩编译(生成 profile 数据)
gcc -O2 -fprofile-generate main.c utils.c -o app_profile

# 步骤 2: 运行程序收集数据(需要代表性输入)
./app_profile --input=typical_data.txt
./app_profile --input=another_data.txt
# 生成 *.gcda 文件

# 步骤 3: 使用 profile 数据重新编译
gcc -O2 -fprofile-use main.c utils.c -o app_optimized

# 步骤 4: 清理 profile 数据(可选)
rm -f *.gcda *.gcno

PGO 与 CMake

option(ENABLE_PGO_GENERATE "Enable PGO profile generation" OFF)
option(ENABLE_PGO_USE "Enable PGO profile use" OFF)

if(ENABLE_PGO_GENERATE)
    add_compile_options(-fprofile-generate)
    add_link_options(-fprofile-generate)
endif()

if(ENABLE_PGO_USE)
    add_compile_options(-fprofile-use -fprofile-correction)
endif()
# 步骤 1: 插桩编译
cmake -B build-pgo-gen -DCMAKE_BUILD_TYPE=Release -DENABLE_PGO_GENERATE=ON
cmake --build build-pgo-gen

# 步骤 2: 运行收集数据
cd build-pgo-gen && ./app < test_input.txt && cd ..

# 步骤 3: 使用 profile 编译
cmake -B build-pgo -DCMAKE_BUILD_TYPE=Release -DENABLE_PGO_USE=ON
cmake --build build-pgo

PGO 效果

指标无 PGO有 PGO
运行速度基准+10% ~ +30%
分支预测准确率较低显著提高
代码布局随机热路径连续
内联决策保守精准

💡 PGO 对于分支密集型代码(如解析器、编译器)效果最显著。

缓存友好数据结构

缓存行与对齐

// cache_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 10000000
#define RUNS 100

// 缓存不友好: AoS (Array of Structures)
struct PointAoS {
    double x, y, z;
    double nx, ny, nz;  // 法线
    double u, v;         // 纹理坐标
};

// 缓存友好: SoA (Structure of Arrays)
struct PointsSoA {
    double *x, *y, *z;
    double *nx, *ny, *nz;
    double *u, *v;
};

// 只访问 x 坐标时,SoA 比 AoS 快得多
// AoS: 每次跳过 64 字节(一个 Point),缓存行利用率 12.5%
// SoA: 连续读取 x 坐标,缓存行利用率 100%

缓存友好的遍历

// ❌ 缓存不友好: 列优先遍历
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += matrix[i][j];  // 每次跳过一整行

// ✅ 缓存友好: 行优先遍历
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += matrix[i][j];  // 连续访问

分支预测友好

// ❌ 分支预测不友好
for (int i = 0; i < N; i++) {
    if (data[i] > 128)   // 数据无序,预测失败率高
        sum += data[i];
}

// ✅ 排序后分支预测友好
qsort(data, N, sizeof(int), compare);
for (int i = 0; i < N; i++) {
    if (data[i] > 128)   // 排序后,预测准确率接近 100%
        sum += data[i];
}

// ✅ 无分支版本(最佳)
for (int i = 0; i < N; i++) {
    int mask = (data[i] - 129) >> 31;  // 算术右移
    sum += data[i] & ~mask;
}

SIMD 优化

自动向量化

# 查看自动向量化报告
gcc -O3 -fopt-info-vec-all simd_test.c -o simd_test 2>&1

# 成功向量化:
# simd_test.c:15:5: note: loop vectorized

# 未向量化原因:
# simd_test.c:20:5: note: loop vectorization failed

手动 SIMD 优化

// simd_manual.c
#include <immintrin.h>
#include <stdio.h>
#include <stdlib.h>

// 使用 AVX2 向量化加法
void add_arrays_avx(float *a, float *b, float *c, int n) {
    int i;
    for (i = 0; i + 8 <= n; i += 8) {
        __m256 va = _mm256_loadu_ps(&a[i]);
        __m256 vb = _mm256_loadu_ps(&b[i]);
        __m256 vc = _mm256_add_ps(va, vb);
        _mm256_storeu_ps(&c[i], vc);
    }
    // 处理剩余元素
    for (; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}

// 使用 SSE 求和
float sum_array_sse(float *arr, int n) {
    __m128 sum = _mm_setzero_ps();
    int i;
    for (i = 0; i + 4 <= n; i += 4) {
        __m128 val = _mm_loadu_ps(&arr[i]);
        sum = _mm_add_ps(sum, val);
    }
    // 水平求和
    float result[4];
    _mm_storeu_ps(result, sum);
    float total = result[0] + result[1] + result[2] + result[3];
    // 处理剩余
    for (; i < n; i++) {
        total += arr[i];
    }
    return total;
}

SIMD 编译选项

# SSE 4.2
gcc -O3 -msse4.2 simd.c -o simd

# AVX2
gcc -O3 -mavx2 simd.c -o simd

# AVX-512
gcc -O3 -mavx512f simd.c -o simd

# 针对当前 CPU 所有支持的指令集
gcc -O3 -march=native simd.c -o simd

# 多版本分发(运行时检测 CPU 支持的指令集)
gcc -O3 -march=x86-64 simd.c -o simd_baseline
gcc -O3 -mavx2 simd.c -o simd_avx2

SIMD 使用建议

场景推荐方案
简单数组运算依赖编译器自动向量化 -O3
复杂运算使用 intrinsics 手动优化
跨平台使用 #ifdef 条件编译
通用方案使用 SIMD 库(如 xsimd, highway)

性能基准测试

Google Benchmark

// benchmark_main.cpp
#include <benchmark/benchmark.h>
#include <vector>
#include <algorithm>
#include <numeric>

// 被测函数
void sum_naive(const std::vector<int>& v, long& result) {
    result = 0;
    for (size_t i = 0; i < v.size(); i++) {
        result += v[i];
    }
}

void sum_iterator(const std::vector<int>& v, long& result) {
    result = std::accumulate(v.begin(), v.end(), 0L);
}

void sum_range(const std::vector<int>& v, long& result) {
    result = 0;
    for (auto x : v) {
        result += x;
    }
}

// 基准测试
static void BM_SumNaive(benchmark::State& state) {
    std::vector<int> v(state.range(0));
    std::iota(v.begin(), v.end(), 1);
    long result;
    for (auto _ : state) {
        sum_naive(v, result);
        benchmark::DoNotOptimize(result);
    }
    state.SetItemsProcessed(state.iterations() * state.range(0));
}

static void BM_SumIterator(benchmark::State& state) {
    std::vector<int> v(state.range(0));
    std::iota(v.begin(), v.end(), 1);
    long result;
    for (auto _ : state) {
        sum_iterator(v, result);
        benchmark::DoNotOptimize(result);
    }
    state.SetItemsProcessed(state.iterations() * state.range(0));
}

// 注册测试
BENCHMARK(BM_SumNaive)->Arg(1000)->Arg(1000000)->Unit(benchmark::kMicrosecond);
BENCHMARK(BM_SumIterator)->Arg(1000)->Arg(1000000)->Unit(benchmark::kMicrosecond);

BENCHMARK_MAIN();
# CMakeLists.txt
find_package(benchmark REQUIRED)
add_executable(bench benchmark_main.cpp)
target_link_libraries(bench PRIVATE benchmark::benchmark)
# 运行基准测试
./bench

# 输出示例:
# ---------------------------------------------------------------
# Benchmark             Time           CPU Iterations UserCounters...
# ---------------------------------------------------------------
# BM_SumNaive/1000     421 ns        420 ns    1663775 items/s=2.38095M/s
# BM_SumNaive/1000000 456210 ns     455890 ns       1534 items/s=2.19351M/s
# BM_SumIterator/1000  418 ns        417 ns    1673321 items/s=2.39808M/s

# JSON 输出
./bench --benchmark_format=json > results.json

# 过滤测试
./bench --benchmark_filter="SumNaive"

综合优化策略

优化决策流程

1. 测量(Profile)→ 找到瓶颈
2. 分析瓶颈类型(CPU/内存/I/O)
3. 选择优化策略
4. 实施优化
5. 重新测量验证

# 永远记住:
# "过早优化是万恶之源" — Donald Knuth
# "但是,不应该忽略那些关键的效率考量" — 后半句

优化优先级

优先级优化手段收益成本
1算法改进10x - 100x
2数据结构优化2x - 10x
3缓存优化2x - 5x
4编译器优化1.1x - 1.5x极低
5SIMD 优化2x - 8x
6多线程并行N x

⚠️ 注意点

  1. Profile 前优化是盲目的:永远先测量再优化
  2. 代表性输入:profiling 必须使用真实的生产数据
  3. -Ofast 浮点精度:科学计算不要用 -Ofast
  4. PGO 需要训练数据:训练数据不具代表性会导致反效果
  5. SIMD 可移植性:手动 SIMD 代码不可移植
  6. LTO 内存消耗:LTO 可能需要大量内存(大型项目可能需要 10GB+)

💡 提示

  1. perf 需要 root 权限sudo sysctl kernel.perf_event_paranoid=1 降低限制
  2. 火焰图最有价值:一张火焰图胜过千行 profile 报告
  3. 编译器报告-fopt-info-vec-all 查看向量化情况
  4. PGO + LTO 组合:两者结合效果最佳
  5. benchmark 库:Google Benchmark 是 C++ 基准测试的标准工具
  6. perf annotate:可以看每条指令的耗时

工程场景

场景 1:排查性能回归

# 步骤 1: 记录当前版本性能
perf record -g ./app_new
perf script | stackcollapse-perf.pl > new.stacks

# 步骤 2: 记录旧版本性能
perf record -g ./app_old
perf script | stackcollapse-perf.pl > old.stacks

# 步骤 3: 生成差分火焰图
difffolded.pl old.stacks new.stacks | flamegraph.pl > diff.svg
# 红色 = 新版本增加的开销
# 蓝色 = 新版本减少的开销

场景 2:优化热循环

# 步骤 1: 找到热点函数
perf record ./app && perf report

# 步骤 2: 查看汇编
perf annotate hot_function

# 步骤 3: 检查是否向量化
gcc -O3 -march=native -fopt-info-vec-all hot.c 2>&1

# 步骤 4: 手动优化或使用 intrinsics

场景 3:CI 性能基准

# .github/workflows/benchmark.yml
name: Performance Benchmark
on: [push, pull_request]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
      - name: Run benchmark
        run: ./build/bench --benchmark_format=json > results.json
      - name: Store benchmark
        uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'googlecpp'
          output-file-path: results.json
          auto-push: true

扩展阅读