第二十四章 性能优化:让 C++ 快起来

← 返回目录

本章目标:学会"先测量再优化"、了解性能关键概念 (缓存、SSO、零开销抽象、算法复杂度)、掌握实际优化手段。 性能是 C++ 的立身之本,但**优化的第一原则是:先别优化**。

24.1 优化圣经:先测量,再动手

程序员最大的错觉:"我知道哪里慢"。 真实情况:90% 的猜测是错的。

正确流程:

  1. 写对的代码(正确性 > 性能)
  2. 用 profiler 测量,找到真正的热点
  3. 只优化热点,优化后重新测量验证

测量工具:

  auto start = std::chrono::steady_clock::now();
  // ... 被测代码 ...
  auto end = std::chrono::steady_clock::now();
  auto us = std::chrono::duration_cast<std::chrono::microseconds>
                (end - start).count();

24.2 算法复杂度优先:数据结构 > 微优化

O(1) vs O(n) vs O(n log n) vs O(n²): 数据量 10 万时,O(n²) 比 O(n log n) 慢 5000 倍。 **优化算法复杂度永远比抠指令划算**。

// 错误:在循环里反复查找
for (int i = 0; i < n; ++i)
    if (std::find(v2.begin(), v2.end(), x[i]) != v2.end()) ...

// 正确:先转成哈希集合,查找变 O(1)
std::unordered_set<int> set2(v2.begin(), v2.end());
for (int x : xs)
    if (set2.contains(x)) ...

常见复杂度对照:

vector 随机访问 / 尾插        O(1)
map 插入/查找                O(log n)
unordered_map 查找            O(1) 平均
sort                         O(n log n)
遍历容器                     O(n)
手写嵌套循环里调用 O(n) 查找  O(n²) ← 最常见的性能杀手

24.3 缓存友好:现代 CPU 的秘密

CPU 比内存快约 100 倍,缓存(L1/L2/L3)就是为了弥合差距。 规则:**顺序访问连续内存 = 快;跳跃访问 = 慢**。

// 快:顺序遍历(缓存命中率高)
long long sum1 = 0;
for (int i = 0; i < n; ++i) sum1 += data[i];

// 慢:跳跃遍历(每步都缓存未命中)
long long sum2 = 0;
for (int i = 0; i < n; i += 1024) sum2 += data[i];   // 注意:只访问了少数
// 更典型的慢法是"按列遍历行优先矩阵",见测试

容器选择与缓存:

vector(连续内存)>> list/deque(节点分散)
游戏行业"CPU 缓存是新的内存"——flat_map 因此比 map 快

矩阵遍历顺序(row-major vs column-major):

// 二维数组按"行优先"存储
// 按行遍历(外层 i 内层 j):快
// 按列遍历(外层 j 内层 i):慢 5~20 倍
测试里有 1024x1024 矩阵对比,可以亲眼看到差异。

24.4 减少拷贝:移动、视图、引用

// 慢:字符串按值传来传去,每次拷贝
std::string slow(std::string s) { return s + "!"; }

// 快:传视图 + 返回构造到目标位置
std::string fast(std::string_view sv) {
    std::string result;
    result.reserve(sv.size() + 1);
    result.append(sv);
    result.push_back('!');
    return result;
}

要点:

  std::vector<int> v;
  v.reserve(1'000'000);          // 避免反复扩容拷贝

v.push_back(std::string("hi")) 少一次移动

24.5 小对象优化(SSO)

std::string 自带 SSO(Small String Optimization): 短字符串(通常 ≤ 15 字节)直接存在对象内部,零堆分配! 所以:

std::string a = "hi";        // SSO:无堆分配
std::string b = a;           // 拷贝也很便宜

24.6 编译选项与优化级别

-O0  不优化(调试用)
-O1  基本优化
-O2  常用发布优化(推荐默认)
-O3  激进优化(循环展开等,可能变大变慢)
-Os  体积优先
-march=native  用本机 CPU 指令(会失去可移植性)
-flto  链接期优化(跨文件内联,发布时开)

发布构建建议:

clang++ -std=c++26 -O2 -flto -DNDEBUG 源文件.cpp -o app

(NDEBUG 关闭 assert)

24.7 零开销抽象(Zero-overhead Principle)

C++ 的原则:"你不用它,就不为它付费"。 例:

代价不是零——是"只为用到的功能付费"。 理解这一点:用标准库不是性能妥协,往往更快。

24.8 常见优化手法速查(按收益排序)

  1. 修算法复杂度(O(n²)→O(n log n):收益最大)
  2. 修缓存局部性(顺序访问、连续内存)
  3. 消除重复计算(把循环内不变的计算提出来)
  4. 减少拷贝(移动/视图/emplace_back/reserve)
  5. 编译期计算(constexpr 表、if constexpr)
  6. 内联热点小函数(lambda + auto 让编译器自由内联)
  7. 避免不必要同步(单线程别加锁)
  8. 释放后重建 vs 复用对象(大循环里)

注意反模式:

24.9 实战:三个可测量的优化示例

(完整代码在测试文件里,直接跑就能看到对比数字)

  1. 矩阵遍历顺序:行优先 vs 列优先(约 5~20 倍差距)
  2. 查找优化:find in vector vs unordered_set(约百倍差距)
  3. 字符串拼接:+ 反复拼接 vs reserve+append(约十倍差距)

24.10 陷阱清单

  1. 用 -O0 测性能
  2. 只测一次就下结论(跑 3 次取中位数)
  3. 优化没改算法,光抠指令
  4. 把"慢"错怪给正确代码(先 profile 再怪)
  5. 用 list 当默认容器(缓存不友好)
  6. 循环里反复分配/释放(提出来复用)
  7. 在热点里用 std::function / 虚函数(可考虑替代)
  8. 多线程数超过核数(线程切换开销)

本章小结

练习题(配套测试:测试_第24章_性能优化.cpp)


  1. 跑矩阵遍历对比,记录两个时间。
  2. 跑 vector vs unordered_set 查找对比。
  3. 跑字符串拼接对比。
  4. 用 reserve 优化循环 push_back,测量差距。
  5. 用 constexpr 把一张表放到编译期,对比运行期计算。