C++ 性能优化手册
适用 C++17 / 20 / 23 · GCC / Clang / MSVC · 更新于 2026-08
性能优化的正确姿势:先用 profiler 找到真正的热点,再按收益从高到低选择手段,最后用基准测试验证。本手册汇总 10 个方向、40 余条可直接落地的优化方案,每条都标注了典型收益与代价。

三步走

目录
§01 测量先行:性能工作的前提 方法论
- 先测量,再优化。 人判断热点的命中率低得惊人;Amdahl 定律决定了只有真正的热点才值得花时间。
- 微基准用 Google Benchmark(或在线的 quick-bench),并用 DoNotOptimize 防止编译器把整个计算消掉。
- 固定测量环境:绑核、关频率抖动、固定输入规模;报告中位数 / p99,不要单次值。
- Debug 构建下测出的性能没有任何参考意义。
| perf | 采样分析:热点函数、分支失效、缓存命中 | Linux |
| Intel VTune | 微架构分析:前端停顿、SIMD 利用率 | Windows / Linux |
| Tracy | 帧级实时 profiler,适合游戏与交互程序 | 跨平台 |
| Callgrind | 精确到指令的调用计数(慢但精确) | Linux / macOS |
| Instruments | 采样、时间线、内存分配分析 | macOS |
| Google Benchmark | 微基准框架,自带防优化与统计 | 跨平台 |
| Compiler Explorer | 在线查看生成汇编、对比两段代码 | Web |
一个不会骗人的微基准:
#include <benchmark/benchmark.h>
static void BM_lookup(benchmark::State& state) {
auto m = build_map(state.range(0)); // 构造放在计时之外
for (auto _ : state) {
benchmark::DoNotOptimize(m.find(42)); // 防止结果被优化掉
}
}
BENCHMARK(BM_lookup)->Arg(1 << 20);
§02 算法与数据结构:最大的杠杆 收益:高
- 复杂度是第一杠杆:O(n²) 降到 O(n log n) 的收益,任何微观技巧都追不上。
- 频繁查找:vector 线性扫描换成 unordered_map 索引;频繁求第 k 大用 nth_element。
- n 很小时反转直觉:几十条数据内,有序 vector + 二分往往比哈希表更快(缓存局部性胜过渐进复杂度)。
- 默认选连续内存容器(vector / deque);std::list 在绝大多数真实场景更慢。
✗ 优化前:每次查询线性扫描 O(n·m)
bool exists(const std::vector<Order>& orders,
int id) {
return std::any_of(orders.begin(), orders.end(),
[&](const Order& o) { return o.id == id; });
}
✓ 优化后:建一次索引,查询 O(1)
std::unordered_map<int, Order> index;
index.reserve(orders.size());
for (auto& o : orders)
index.emplace(o.id, std::move(o));
// 之后每次查询:index.find(id)
§03 内存布局与缓存 收益:高
- 现代 CPU 里一次缓存未命中约几十到上百周期;热点循环优先保证顺序、连续访问。
- AoS → SoA:把参与计算的字段单独成列,缓存行里不装用不到的数据。
- 伪共享:多线程写相邻变量会互相拖累,按缓存行(一般 64B)对齐隔离。
- 小对象频繁 new/delete:改用内存池 / arena(std::pmr),整块分配、整体释放。
- 定期用 sizeof 检查热结构体:删掉用不到的成员,注意对齐 padding 带来的虚胖。


✗ AoS:只用 pos/vel 却搬进整个结构体
struct Particle {
Vec3 pos, vel;
float mass;
char unused[48]; // 用不到的字段
};
std::vector<Particle> ps;
✓ SoA:缓存行里只装参与计算的字段
struct Particles {
std::vector<Vec3> pos;
std::vector<Vec3> vel;
std::vector<float> mass;
};
伪共享与内存池:
// 伪共享:8 个计数器挤在一起,多核写入互抢同一条缓存行
struct Counters { std::atomic<uint64_t> hit[8]; };
// 对齐到缓存行,各自独占
struct Counters {
struct alignas(64) Slot { std::atomic<uint64_t> v{}; };
std::array<Slot, 8> hit;
};
// arena 内存池:顺序分配、整体析构,避开逐条 new/delete
char buf[1 << 20];
std::pmr::monotonic_buffer_resource arena(buf, sizeof buf);
std::pmr::vector<Node> nodes{&arena};
§04 拷贝与移动:先消灭拷贝再谈别的 收益:高
- 返回局部对象直接 return x;——RVO 保证零拷贝;画蛇添足的 return std::move(x) 反而可能阻止 RVO。
- 传参:大的只读对象用 const&;要拿走所有权时用值传递 + std::move。
- 只读字符串接口用 std::string_view,不拷贝且兼容 string / 字面量 / 子串(注意生命周期)。短字符串有 SSO,本来就不分配堆内存。
- 字符串拼接用 += / append,别用 s = s + a + b。
✗ 不必要的深拷贝
void send(std::string payload); // 按值传参
std::string buffer = build_report();
send(buffer); // 深拷贝整个字符串
✓ 转移所有权 + RVO
send(std::move(buffer)); // 只转移内部指针
Widget make() {
Widget w;
/* … */
return w; // RVO,零拷贝
}
字符串拼接:
// 每步都生成临时串,近似 O(n²) 的拷贝
s = s + a + b + c; // ✗
// 原地追加,摊销 O(n)
s += a; s += b; s += c; // ✓
§05 容器与标准库细节 收益:中 – 高
- vector 扩容 = 翻倍重分配 + 全量搬迁:已知规模先 reserve()。
- 删除元素用 erase–remove 一次遍历;C++20 直接 std::erase_if。
- unordered_map:reserve 预分配桶 + 调 max_load_factor,避免中途 rehash。
- 读多写少的键值场景:C++23 std::flat_map(有序 vector 实现,缓存友好)。
- 容器里构造元素用 emplace_back 原地构造;只读视图传 std::span;小定长缓冲用 std::array。
预分配与原地构造:
std::vector<Record> out;
out.reserve(input.size()); // 免去 log₂n 次扩容拷贝
std::vector<UserProfile> users;
users.emplace_back(id, name, age); // ✓ 原地构造
users.push_back(UserProfile(id, name, age)); // 对比:先造临时对象再移动
std::erase_if(v, pred); // ✓ C++20 一次遍历删除
v.erase(std::remove_if(v.begin(), v.end(), pred), v.end()); // C++17 写法
关联容器调优:
std::unordered_map<Key, Val> m;
m.reserve(n); // 预分配桶,避免 rehash
m.max_load_factor(0.7f);
std::flat_map<Key, Val> fm; // C++23:读多写少比红黑树更快
§06 并发与锁 收益:高(并发场景)
- 锁的成本主要来自竞争:缩小临界区、降低加锁频率,比换"更快的锁"有效得多。
- 分片(sharding):按 key 哈希到 N 个独立锁,竞争概率降为约 1/N。
- 读写比例悬殊用 shared_mutex;无竞争的简单状态用 atomic。
- 生产者–消费者:有界队列 + 批量提交,把 1000 次加锁摊成 1 次;无锁队列只在 profile 证明值得时用。
- 并行计算:std::execution::par(libstdc++ 需 TBB)/ OpenMP / TBB;注意划分粒度与伪共享(见 §03)。
- 线程创建昂贵:用线程池,别按任务现开线程。
分片锁示例:
struct alignas(64) Shard { // 顺带避开伪共享
std::mutex mu;
std::unordered_map<Key, Val> data;
};
std::array<Shard, 64> shards;
Shard& s = shards[std::hash<Key>{}(k) % shards.size()];
{
std::lock_guard lk(s.mu); // 临界区只包住一次读写
s.data[k] = v;
}
§07 编译器与构建:免费的百分比 收益:中 – 高
- 永远在 -O2(MSVC /O2)以上测性能;-O3 偶尔更快,需实测。
- -march=native 解锁本机 AVX2 / AVX-512 等指令;跨平台分发时权衡。
- LTO 跨编译单元内联与优化,通常白捡 2%–8%。
- PGO:插桩采集真实负载再反馈编译,热路径提升常达 10%+;BOLT 可在链接后再做一层布局优化。
- final / override 帮助去虚化与内联;彻底消除虚调用可用模板 / CRTP 静态多态。
- [[likely]] / [[unlikely]] 标注分支倾向;__restrict 承诺无别名,帮助向量化。
- 异常在主流 ABI 下是"不抛出即零成本"的表式实现;成本在真正 throw 的展开路径,热路径上不要 throw。
| -O2 / -O3 | /O2 | 基础优化等级(测性能的前提) |
| -march=native | /arch:AVX2 | 启用本机指令集 |
| -flto | /GL + /LTCG | 链接期优化(LTO) |
| -fprofile-generate → -fprofile-use | /GENPROFILE → /USEPROFILE | PGO:按真实负载优化 |
| -fopt-info-vec-missed | /Qvec-report:1 | 查看为什么没被自动向量化 |
给编译器的提示:
struct Renderer final : IRenderer { }; // final 帮助去虚化、内联
if (n >= 0) [[likely]] { // C++20 分支提示
fast_path(n);
} else {
slow_path(n);
}
void blend(const float* __restrict src, // 无别名承诺,帮助向量化
float* __restrict dst, size_t n);
§08 向量化与 SIMD 收益:视热点而定
- 先让编译器自动向量化:循环要简单、连续内存、无数据依赖、无别名。
- 用向量优化报告(见 §07 表格)看编译器为什么没向量化,逐条消除障碍。
- 浮点循环若允许重排,可开 -ffast-math——注意它会改变 NaN / 结合律语义,数值程序要验证。
- 手动 SIMD(intrinsics / SIMD 库)是最后手段:只在 profiler 证明值得时做。
- alignas(32/64) 对齐 + 定长批次更利于生成向量指令。
可自动向量化的循环:
void axpy(float a, const float* __restrict x,
float* __restrict y, size_t n) {
for (size_t i = 0; i < n; ++i)
y[i] += a * x[i]; // 连续内存、无别名、无复杂控制流
}
§09 I/O 与其他 收益:中
- 大量 iostream 读写前:std::ios::sync_with_stdio(false) + cin.tie(nullptr)。
- 换行用 '\\n',std::endl 每次都 flush。
- 大文件一次性读入或用块读 / mmap,别逐 token 用 >> 提取;输出先攒缓冲再一次性写。
- 格式化:std::format(C++20)明显快于 iostream;极限场景用 std::to_chars。
- std::regex 构造昂贵:编译一次复用;热路径解析考虑手写。
std::ios::sync_with_stdio(false); // 与 C stdio 解绑
std::cin.tie(nullptr); // 解除 cin→cout 的隐式 flush
std::cout << x << '\\n'; // 用 '\\n',别用 std::endl
// 大文件一次读入
std::ifstream f(path, std::ios::binary);
std::string buf((std::istreambuf_iterator<char>(f)), {});
§10 速查清单与常见减速带 速查
| reserve 预分配容量 | §05 | 高 | 几乎无代价 |
| 哈希索引替代线性扫描 | §02 | 高 | 用内存换时间 |
| 消除拷贝:移动 + RVO | §04 | 高 | 理清所有权语义 |
| SoA / 紧凑内存布局 | §03 | 高 | 需要重构数据结构 |
| 分片降低锁竞争 | §06 | 高 | 仅并发场景,复杂度上升 |
| PGO + LTO | §07 | 中 – 高 | 构建流程变复杂 |
| 批量 I/O、关 sync | §09 | 中 | 几乎无代价 |
| string_view 传参 | §04 | 中 | 生命周期要管好 |
| final / 静态多态去虚化 | §07 | 低 – 中 | 放弃运行时多态 |
| 手写 SIMD | §08 | 最后手段 | 先证明值得,注意可移植性 |
常见减速带 · 看到就顺手改掉
- ✗ 热路径字符串拼接 s = s + a + b,应改 +=。
- ✗ 大量输出时用 std::endl 代替 '\\n'。
- ✗ std::vector<bool>:位压缩代理,行为不像正常容器。
- ✗ 每次使用都重新构造 std::regex。
- ✗ 热路径上 std::function + shared_ptr 的组合(间接跳转 + 原子计数)。
- ✗ return std::move(局部变量),破坏 RVO。
- ✗ 在 Debug 构建下做性能判断。
“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.”
— Donald Knuth, 1974
反过来说:剩下那 3% 的热点,值得按本手册从头到尾收拾一遍。先测量,再优化;优化之后,再测量。


