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 | 极低 |
| 5 | SIMD 优化 | 2x - 8x | 中 |
| 6 | 多线程并行 | N x | 高 |
⚠️ 注意点
- Profile 前优化是盲目的:永远先测量再优化
- 代表性输入:profiling 必须使用真实的生产数据
- -Ofast 浮点精度:科学计算不要用
-Ofast - PGO 需要训练数据:训练数据不具代表性会导致反效果
- SIMD 可移植性:手动 SIMD 代码不可移植
- LTO 内存消耗:LTO 可能需要大量内存(大型项目可能需要 10GB+)
💡 提示
- perf 需要 root 权限:
sudo sysctl kernel.perf_event_paranoid=1 降低限制 - 火焰图最有价值:一张火焰图胜过千行 profile 报告
- 编译器报告:
-fopt-info-vec-all 查看向量化情况 - PGO + LTO 组合:两者结合效果最佳
- benchmark 库:Google Benchmark 是 C++ 基准测试的标准工具
- 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
扩展阅读