跳到主要内容

C++ 性能优化实战清单

性能优化的第一原则:先测量,再优化。没有 profile 数据的优化,通常只是让代码变丑。

一、正确的优化顺序​

  1. 确定目标:是延迟、吞吐还是内存?三者往往互相牵制;
  2. 建立基准:可重复的 benchmark + 真实数据规模;
  3. profile 找热点:perf、valgrind --tool=callgrind、gprof;
  4. 只改热点:非热点代码的"优化"是纯负债;
  5. 验证:改完再测,确认真的快了,且没有功能退化。
# Linux 采样分析(最常用)
perf record -g ./app
perf report

# 缓存未命中分析
perf stat -e cache-misses,cache-references,instructions,cycles ./app

# 调用图与热点
valgrind --tool=callgrind ./app && callgrind_annotate callgrind.out.*

二、编译选项层面​

选项作用注意
-O2通用优化,首选编译时间与性能平衡
-O3更强向量化与内联代码体积涨,可能变慢
-march=native启用本机指令集产物不可跨机器分发
-flto跨编译单元内联需链接时也加,构建变慢
-fno-exceptions去掉异常支持只能用于无异常代码
-DNDEBUG关闭 assertRelease 必加
-g + -O2保留调试信息生产环境也建议保留,便于符号化

常见失误:Release 构建忘了 -DNDEBUG,导致 assert 里的表达式仍在求值。

三、数据结构与算法​

这是收益最大的一层,通常能带来 2~100 倍改善。

  • 把 O(n²) 的嵌套循环改成哈希表索引;
  • 频繁遍历改数组代替链表;
  • 小规模数据用 vector + 线性查找,别上 map;
  • 需要"按键查找 + 有序遍历"时,考虑排序 vector + lower_bound;
  • 避免 vector<vector<T>>,改扁平化一维数组 + 偏移表。
// 扁平化:稀疏二维数据
std::vector<int> offsets; // 每行起始下标
std::vector<Row> rows; // 所有行连续存放

四、缓存友好性​

现代 CPU 上,内存访问的代价远高于算术运算。

原则 1:顺序访问优于随机访问​

// ❌ 对缓存极不友好
for (std::size_t i = 0; i < n; ++i)
for (std::size_t j = 0; j < n; ++j)
sum += mat[j][i];

// ✅ 按行遍历(假设行连续)
for (std::size_t i = 0; i < n; ++i)
for (std::size_t j = 0; j < n; ++j)
sum += mat[i][j];

原则 2:数据布局分离热冷字段​

struct Particle {
double x, y, z; // 热:每帧更新
std::string name; // 冷:几乎不读
std::array<char, 64> meta;
}; // ❌ 遍历时把冷数据也拖进缓存

// ✅ 拆成两个数组
struct ParticlePos { double x, y, z; };
std::vector<ParticlePos> pos;
std::vector<ParticleInfo> info;

原则 3:结构体数组 vs 数组结构(SoA)​

需要向量化时,SoA 通常更快:

// AoS:每个元素连续
struct P { float x, y, z; };
std::vector<P> ps;

// SoA:每个分量为独立数组,便于 SIMD
struct Ps { std::vector<float> x, y, z; };

五、减少分配与拷贝​

// ❌ 循环内反复分配
for (int i = 0; i < n; ++i) {
std::vector<int> tmp = compute(i);
consume(tmp);
}

// ✅ 复用缓冲区
std::vector<int> tmp;
for (int i = 0; i < n; ++i) {
tmp.clear();
computeInto(i, tmp);
consume(tmp);
}

其他要点:

  • 提前 reserve;
  • 返回值依赖 RVO,不要 return std::move(x);
  • 传参用 const&,需要接管时按值 + std::move;
  • 避免在小对象上做堆分配(考虑 std::array、内联缓冲、pmr 内存池)。

六、字符串与 I/O​

  • 拼接用 += 或 append,不要 s = s + x;
  • 只读子串用 std::string_view(C++17);
  • 输出密集场景关闭同步:
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
  • 大文件读写用缓冲区(read/write 批量),不要逐字节;
  • 日志用异步队列,别在热点路径里加锁写文件。

七、分支与虚函数​

  • 热点里高频、可预测的分支影响不大,不可预测的分支代价高(分支预测失败 ~10+ 周期);
  • 虚函数一次间接跳转 + 阻止内联,热路径可考虑模板化或 std::variant + visit;
  • 避免在热循环里用 std::function(类型擦除 + 可能堆分配),改模板参数。

八、多线程优化​

  • 先看是否真的受限于 CPU:perf stat 看 IPC 与 CPU 利用率;
  • 拆分任务粒度要足够大(线程创建与同步有成本);
  • 无共享优于加锁共享;
  • 注意伪共享:相邻的热点计数器用 alignas(64) 隔开;
  • 用 TSan 确认无数据竞争,用 perf c2c 查伪共享。

九、优化清单(按投入产出排序)​

顺序项目典型收益
1算法与数据结构2~100 倍
2消除重复分配/拷贝1.2~5 倍
3缓存友好布局1.2~3 倍
4编译选项(-O2/-flto/-march)1.1~2 倍
5并行化接近核数(有上限)
6手写 SIMD2~8 倍(特定场景)
7微优化(位运算、查表)5%~30%

十、反面清单​

  • 不要用 register、inline 关键字"提示"编译器(现代编译器自己判断);
  • 不要为省一个拷贝把代码写成不可读的样子,先 profile;
  • 不要在 -O0 下做性能结论;
  • 不要用 std::endl 代替 "\n"(前者每次都 flush);
  • 不要在没有基准的情况下相信"优化"。

十一、建立基准的习惯​

#include <chrono>

auto t0 = std::chrono::steady_clock::now();
runBenchmark();
auto t1 = std::chrono::steady_clock::now();
std::cout << std::chrono::duration<double, std::milli>(t1 - t0).count() << " ms\n";

更严谨的基准需要:预热、多次取中位数、固定 CPU 频率、关闭其他负载。工具推荐 google/benchmark 或 nanobench。

性能是设计出来的副产品,不是最后一步的补丁。