C++ 性能优化实战清单
性能优化的第一原则:先测量,再优化。没有 profile 数据的优化,通常只是让代码变丑。
一、正确的优化顺序
- 确定目标:是延迟、吞吐还是内存?三者往往互相牵制;
- 建立基准:可重复的 benchmark + 真实数据规模;
- profile 找热点:
perf、valgrind --tool=callgrind、gprof; - 只改热点:非热点代码的"优化"是纯负债;
- 验证:改完再测,确认真的快了,且没有功能退化。
# 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 | 关闭 assert | Release 必加 |
-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 | 手写 SIMD | 2~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。
性能是设计出来的副产品,不是最后一步的补丁。