C++ 开发手册C++

CHAPTER 09生产层 · PERFORMANCE

性能优化

缓存友好、move 语义、向量化、profiling 与 benchmark。先度量再优化,用现代 C++ 特性榨取硬件性能。

开始阅读难度高阶先修全本手册约2.5小时
09.1

先度量再优化

性能优化第一条铁律:用数据定位瓶颈,别凭直觉。盲目优化既浪费时间又可能让代码变复杂还更慢。

二八定律

通常 20% 的代码消耗 80% 的时间。profiler 帮你精准找到那 20%,把精力用对地方。

09.2

缓存与局部性

CPU 缓存远快于内存。连续访问(vector/数组)命中缓存;链表、散乱指针导致缓存未命中,性能骤降。

// 友好:连续内存顺序访问
for (const auto& x : vec) sum += x;
// 不友好:链表跳跃访问,缓存未命中多
for (auto it = list.begin(); it != list.end(); ++it) sum += *it;
结构体数组 vs 数组结构体

若只用到部分字段,SoA(结构体数组)更缓存友好、利于向量化;AoS(数组结构体)则更直观,按访问模式取舍。

09.3

move 语义

C++11 的右值引用与 std::move 允许"移动"而非拷贝资源(如字符串、容器底层缓冲),把昂贵复制变成指针交换。

std::vector<int> make() {
  std::vector<int> v(1'000'000);
  return v;                 // 具名对象 NRVO/移动,不拷贝
}
std::vector<int> a = make();
std::vector<int> b = std::move(a);  // a 被掏空,b 接管缓冲
move 后被掏空

被 std::move 后的对象处于有效但未指定状态,只能赋值/析构,别再读取其值。

09.4

避免拷贝

传递大对象用 const T&;返回大对象靠返回值优化(RVO/NRVO);用 reserve 避免容器反复扩容。

void process(const BigData& d);     // 不拷贝
std::string build() {
  std::string s;
  s.reserve(1024);                   // 避免多次分配
  return s;                          // RVO 移动
}
09.5

向量化 SIMD

编译器常自动向量化循环(一次处理多个数据)。编写"简单、无分支、连续访问"的循环更利于自动 SIMD;必要时用 #pragma omp simd 或 intrinsics。

// 简单无依赖循环,编译器易向量化
for (size_t i = 0; i < n; ++i)
  c[i] = a[i] + b[i];
别阻碍优化

循环里别调用有副作用/虚函数、别有跨迭代依赖,否则编译器不敢向量化。

09.6

Profiling

用 perf(Linux)、Instruments(macOS)、VTune 做采样/插桩分析,看热点函数与缓存命中率,定位真正瓶颈。

perf record ./app
perf report            # 交互式查看热点
09.7

微观基准

用 Google Benchmark 写微基准,注意避免"死代码消除"(把结果 print 出来),并在稳定环境下多次取中位数。

static void BM_Sum(benchmark::State& s) {
  for (auto _ : s) {
    volatile int x = compute();   // 防止被优化掉
  }
}
BENCHMARK(BM_Sum);
09.8

常见陷阱与 FAQ

为什么换了更"快"的算法反而更慢?

可能新算法缓存不友好或分支多,抵消了渐进复杂度的优势。对小规模数据,常数因子与局部性往往更关键——用基准验证。

std::move 一定更快?

仅当对象拥有昂贵资源(堆缓冲)时移动才划算。对 int 等小对象,移动等同拷贝,无收益。

基准结果忽高忽低?

受 CPU 频率调节、后台进程、缓存 warm-up 影响。固定 CPU 频率、预热、多次取中位数,关闭无关进程。

09.9

实战练习与资源

延伸资源