C++性能优化
C++性能优化的理论、方法、工具与工程实践
学习目标
本节按照 Bloom 分类法的认知层级组织学习目标,读者完成本章学习后应能够达到以下层级:
识记层 (Remembering)
- 列举 C++性能优化的主要层次(算法层、系统层、微架构层、编译器层)
- 复述 Amdahl 定律与 Gustafson 定律的数学形式
- 说出 CPU 缓存层级(L1/L2/L3/LLC)的典型容量与访问延迟量级
- 识别主流性能分析工具(name、perf、VTune、Tracy、gperftools)的用途
理解层 (Understanding)
- 解释数据局部性(data locality)与缓存命中率对性能的影响机制
- 阐述分支预测与分支预测失败对流水线的影响
- 描述编译器优化级别(-O0/-O1/-O2/-O3/-Os/-Ofast/-Oz)的取舍
- 区分测量偏差(measurement bias)与微基准陷阱(microbenchmark pitfall)
应用层 (Applying)
- 使用
perf工具采集程序的热点函数与缓存未命中统计 - 使用
std::chrono与 Google Benchmark 编写可重复的微基准 - 通过
__builtin_expect、[[likely]]/[[unlikely]]调整分支提示 - 应用 SOA(struct of arrays)布局重构热数据结构以提升向量化效率
分析层 (Analyzing)
- 对比循环展开、循环分块(loop tiling)、循环交换(loop interchange)的适用场景
- 解构一段性能不达标的代码,定位是算法、内存、还是分支问题
- 分析火焰图识别调用栈中的热点与冗余调用链
评价层 (Evaluating)
- 评估一次优化是否真正带来净收益(考虑可读性、可维护性、可移植性)
- 判断 Profile-Guided Optimization (PGO) 与 LTO 在具体项目中的投入产出比
- 在 SIMD intrinsics、auto-vectorization、编译器 pragma 之间做出工程权衡
创造层 (Creating)
- 设计一套面向高频交易系统的低延迟数据通路,纳秒级响应
- 构建可复用的性能回归 CI 流水线,自动对比基线并阻断劣化
- 提出一套面向团队的性能优化规范,涵盖测量、分析、回归、文档化全流程
历史动机与背景
性能优化是 C++ 区别于其他高级语言的核心价值主张之一。回溯历史,C++ 性能优化的发展可被划分为若干阶段,每一阶段都对应着硬件与软件范式的变迁。
1960-1980:汇编时代与硬件意识
早期计算资源极其稀缺,程序员普遍使用汇编甚至机器码编程,对寄存器、内存、外设的每一次访问都需要精确考量。这一时期奠定了”硬件意识”传统,即程序员需要对底层硬件有深刻理解。FORTRAN 与 COBOL 虽为高级语言,但性能仍是核心议题。
1980-2000:C++ 的诞生与抽象代价之争
Bjarne Stroustrup 在 1979-1985 年间发明 C with Classes,后演进为 C++。其核心理念是”零开销抽象”(zero-overhead abstraction):你不为你不使用的东西付出代价,你使用的东西手工写也写不更好。这一理念直接对抗当时 Smalltalk、Java 等语言带来的运行时开销。
然而,“抽象是否真的零开销”长期存在争议。1994 年 Scott Meyers 出版《Effective C++》系统化讨论 C++ 性能议题;2000 年前后,Stan Lippman、Josée Lajoie 在《C++ Primer》中也开始强调性能考量。
2000-2010:多核革命与缓存感知编程
Herb Sutter 在 2005 年发表《The Free Lunch Is Over》一文,宣告单核频率提升的物理极限已至,多核时代正式来临。这一转变带来两大影响:
- 顺序程序”免费变快”的红利消失,并发与并行成为提升性能的必由之路
- 缓存层级与 NUMA 架构愈发复杂,缓存未命中代价占总延迟比例上升
此时期出现了 Agner Fog 的 CPU 优化手册、Ulrich Drepper 的《What Every Programmer Should Know About Memory》等里程碑文献。
2010-2020:编译器与向量化时代
GCC 与 Clang 持续提升自动向量化能力,LTO 与 PGO 进入主流。Intel 推出 AVX2/AVX-512 指令集,服务器与 HPC 场景开始普及 512-bit 向量宽度。同时,C++11/14/17 引入了 constexpr、移动语义、std::aligned_alloc 等为性能服务的语言特性。
Chandler Carruth 在 CppCon 多次演讲揭示现代编译器优化的真实行为,推动社区重新理解”-O2 神话”。
2020 至今:异构计算与端到端延迟优化
随着 GPU、TPU、NPU、FPGA 等异构加速器普及,C++ 性能优化从单一 CPU 视角扩展到异构体系。同时,云原生与微服务架构推动端到端延迟优化成为新热点,p99、p999 等尾延迟指标进入工程视野。
CppCon 持续涌现高质量性能议题,如 std::simd 提案、std::execution 并发框架、std::expected 减少异常开销等。Bloomberg、Meta、Google、AWS 等公司公开分享了其在低延迟、大规模系统中的 C++ 优化经验。
核心动因总结
C++ 性能优化的根本动因可归纳为三点:
- 资源稀缺性:无论是嵌入式、移动端还是服务器,资源(算力、内存、能耗、带宽)始终有限
- 经济性:在 Web Scale 场景,1% 的性能提升可节省数百万美元服务器成本
- 可竞争性:在交易、游戏、广告竞价等场景,纳秒级延迟直接决定商业胜负
形式化定义
程序执行时间的分解模型
程序的墙钟执行时间 可被分解为以下分量:
其中:
- :CPU 计算时间(包含指令执行与流水线消耗)
- :内存访问时间(包含缓存命中与未命中)
- :I/O 等待时间(磁盘、网络、外设)
- :线程同步等待时间(锁、屏障、原子操作)
- :操作系统调度与上下文切换时间
CPU 计算时间 可进一步细化:
其中 为执行的指令总数(Instruction Count), 为每条指令平均周期数(Cycles Per Instruction), 为流水线停顿时间。这一公式引出了性能优化的两大方向:减少指令数(算法优化、内联)与降低 CPI(向量化、减少分支、改善数据局部性)。
内存访问的时间模型
CPU 访问内存的时间 可被建模为缓存层级概率加权:
其中 为第 层缓存命中概率(满足 ), 为第 层访问延迟。例如,典型桌面 CPU 的延迟量级为:
| 层级 | 延迟(周期) | 延迟(纳秒,3GHz) |
|---|---|---|
| 寄存器 | 0 | 0 |
| L1 cache | 4 | 1.3 |
| L2 cache | 12 | 4 |
| L3 cache | 40 | 13 |
| 主内存 | 200 | 67 |
| NUMA 远端 | 300 | 100 |
| NVMe SSD | 30000 | 10000 |
由此可见,内存访问延迟跨度达 4 个数量级,数据局部性对性能影响极其显著。
Amdahl 定律
Amdahl 定律刻画了加速比 与可并行部分占比 及处理器数 之间的关系:
当 ,,即加速比受串行部分 严格上界。这意味着若 5% 的代码必须串行执行,则理论最大加速比不超过 20 倍。
Gustafson 定律
Gustafson 定律反驳了 Amdahl 定律的悲观结论,指出在规模化场景下,问题规模通常随处理器数增加而增大,并行部分占比也随之上升:
其中 为串行部分占比。Gustafson 定律适用于 HPC、批处理等可扩展问题,而 Amdahl 定律更适合固定规模问题。
Roofline 模型
Roofline 模型将算力峰值与带宽峰值综合,刻画应用的性能上限:
其中 为实际性能(FLOP/s), 为算力峰值(FLOP/s), 为内存带宽(FLOP/s), 为算术强度(Arithmetic Intensity,单位 FLOP/Byte)。该模型指出,低算术强度应用受带宽限制,高算术强度应用受算力限制。
Little 定律在性能中的应用
Little 定律在排队系统中表述为:
其中 为系统中平均任务数, 为到达速率, 为平均逗留时间。在性能工程中,该定律可用于估算线程池大小、连接池容量等关键参数。
理论推导
缓存友好访问模式的复杂度分析
考虑矩阵遍历问题。设有 矩阵,缓存行大小为 字节,元素大小为 字节,则每行可容纳 个元素。
行优先遍历的缓存未命中次数
行优先遍历每访问 个元素才产生一次缓存未命中,总未命中次数为:
列优先遍历的缓存未命中次数
若矩阵行数 且矩阵未在缓存中,列优先遍历每次访问都会产生未命中(假设缓存不大于一行):
设 ,(64 字节缓存行, 字节 double),则:
实测中,这一差距常达 5-10 倍,与缓存容量与替换策略有关。
循环分块的复杂度推导
考虑 矩阵乘法 。朴素实现为三重循环,缓存未命中数 。引入分块大小 后,缓存未命中数为:
对 求导求极小值,得最优块大小:
更精确的分析需考虑缓存容量 ,通常选择 使三个分块同时驻留缓存:,故 。
分支预测失败的代价推导
设分支预测准确率为 ,预测失败代价为 (典型 15-20 周期),分支占比为 。则每条指令平均分支代价为:
若 ,,,则 ,占 CPI 5%-10%。在数据驱动分支(如排序后的 if-else)中, 可能跌至 0.5, 升至 1.8,显著恶化性能。
向量化的理论上限
设向量宽度为 (如 AVX2 为 8 个 float),则理论加速比为 。但实际加速比受以下因素制约:
其中 为向量加载、对齐、尾部处理开销。当 ,,通常 ,故渐近加速比接近 。但小循环或非连续数据下, 占比上升,实际加速比可能仅 2-4 倍。
Lock-Free 与 Lock-Based 的复杂度对比
设临界区执行时间为 ,锁开销为 ,线程数为 。Lock-based 串行化执行,总时间:
Lock-free 基于 CAS,失败重试概率 (随 上升):
低争用下 ,;高争用下 趋近 1,lock-free 可能反而退化。这解释了为何 lock-free 适合低争用、低延迟场景,而非”银弹”。
代码示例
示例 1:矩阵遍历的缓存效应
// 文件: matrix_traversal.cpp
// 演示行优先与列优先遍历的性能差距
// 编译: g++ -O2 -std=c++17 matrix_traversal.cpp -o matrix_traversal
#include <chrono>
#include <iostream>
#include <vector>
// 性能计时辅助类
// 使用 RAII 在作用域结束时自动输出耗时
class Timer {
public:
explicit Timer(std::string label) : label_(std::move(label)), start_(std::chrono::high_resolution_clock::now()) {}
~Timer() {
auto end = std::chrono::high_resolution_clock::now();
auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start_).count();
std::cout << label_ << ": " << ns / 1e6 << " ms\n";
}
private:
std::string label_;
std::chrono::high_resolution_clock::time_point start_;
};
// 行优先遍历:缓存友好
// 每次内层循环顺序访问连续内存,缓存命中率高
double sumRowMajor(const std::vector<std::vector<double>>& matrix) {
double sum = 0.0;
for (size_t i = 0; i < matrix.size(); ++i) {
for (size_t j = 0; j < matrix[i].size(); ++j) {
sum += matrix[i][j];
}
}
return sum;
}
// 列优先遍历:缓存不友好
// 内层循环跨行跳跃,每访问一个元素都可能产生缓存未命中
double sumColMajor(const std::vector<std::vector<double>>& matrix) {
double sum = 0.0;
for (size_t j = 0; j < matrix[0].size(); ++j) {
for (size_t i = 0; i < matrix.size(); ++i) {
sum += matrix[i][j];
}
}
return sum;
}
int main() {
const size_t N = 4096;
std::vector<std::vector<double>> matrix(N, std::vector<double>(N, 1.0));
// 预热缓存
volatile double warmup = sumRowMajor(matrix);
(void)warmup;
{
Timer t("Row-major");
volatile double result = sumRowMajor(matrix);
(void)result;
}
{
Timer t("Col-major");
volatile double result = sumColMajor(matrix);
(void)result;
}
return 0;
}
典型输出(4096x4096 double 矩阵):
Row-major: 18.5 ms
Col-major: 152.3 ms
约 8 倍差距,与理论推导一致。
示例 2:循环分块优化的矩阵乘法
// 文件: matrix_multiply.cpp
// 演示朴素实现与分块优化的性能对比
// 编译: g++ -O3 -mavx2 -std=c++17 matrix_multiply.cpp -o matrix_multiply
#include <chrono>
#include <iostream>
#include <vector>
constexpr size_t BLOCK_SIZE = 64; // 分块大小,根据 L1 缓存容量调整
// 朴素三重循环实现
// 时间复杂度 O(n^3),缓存未命中频繁
void naiveMultiply(const std::vector<double>& A,
const std::vector<double>& B,
std::vector<double>& C,
size_t n) {
for (size_t i = 0; i < n; ++i) {
for (size_t j = 0; j < n; ++j) {
double sum = 0.0;
for (size_t k = 0; k < n; ++k) {
sum += A[i * n + k] * B[k * n + j];
}
C[i * n + j] = sum;
}
}
}
// 分块实现
// 将矩阵划分为 BLOCK_SIZE x BLOCK_SIZE 的子块
// 子块可同时驻留 L1 缓存,显著降低缓存未命中
void blockedMultiply(const std::vector<double>& A,
const std::vector<double>& B,
std::vector<double>& C,
size_t n) {
for (size_t ii = 0; ii < n; ii += BLOCK_SIZE) {
for (size_t jj = 0; jj < n; jj += BLOCK_SIZE) {
for (size_t kk = 0; kk < n; kk += BLOCK_SIZE) {
// 处理一个子块
size_t i_end = std::min(ii + BLOCK_SIZE, n);
size_t j_end = std::min(jj + BLOCK_SIZE, n);
size_t k_end = std::min(kk + BLOCK_SIZE, n);
for (size_t i = ii; i < i_end; ++i) {
for (size_t k = kk; k < k_end; ++k) {
double aik = A[i * n + k];
for (size_t j = jj; j < j_end; ++j) {
C[i * n + j] += aik * B[k * n + j];
}
}
}
}
}
}
}
int main() {
const size_t N = 1024;
std::vector<double> A(N * N, 1.0), B(N * N, 2.0), C1(N * N, 0.0), C2(N * N, 0.0);
using namespace std::chrono;
auto t1 = high_resolution_clock::now();
naiveMultiply(A, B, C1, N);
auto t2 = high_resolution_clock::now();
blockedMultiply(A, B, C2, N);
auto t3 = high_resolution_clock::now();
std::cout << "Naive: " << duration_cast<milliseconds>(t2 - t1).count() << " ms\n";
std::cout << "Blocked: " << duration_cast<milliseconds>(t3 - t2).count() << " ms\n";
return 0;
}
典型输出:
Naive: 4250 ms
Blocked: 1180 ms
约 3.6 倍加速,源于缓存命中率的显著提升。
示例 3:分支预测优化
// 文件: branch_prediction.cpp
// 演示排序前后分支预测对性能的影响
// 编译: g++ -O2 -std=c++17 branch_prediction.cpp -o branch_prediction
#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <vector>
// 不可预测分支:阈值过滤
// 当数据分布随机时,分支预测准确率约 50%,产生大量流水线冲刷
long long sumIfUnsorted(std::vector<int>& data) {
long long sum = 0;
for (auto x : data) {
if (x < 128) { // 阈值固定,数据随机
sum += x;
}
}
return sum;
}
// 可预测分支:数据已排序
// 排序后,前半段全部命中,后半段全部不命中,预测准确率接近 100%
long long sumIfSorted(std::vector<int>& data) {
long long sum = 0;
for (auto x : data) {
if (x < 128) {
sum += x;
}
}
return sum;
}
// 分支消除:位运算替代条件分支
// 利用算术替代分支,完全避免分支预测失败
long long sumBranchless(std::vector<int>& data) {
long long sum = 0;
for (auto x : data) {
// (x < 128) 为 true 时 mask = 0xFFFFFFFF,否则 0
// 与运算选择性地将 x 加入 sum
sum += (x < 128) * x; // 借助布尔到整型的隐式转换
}
return sum;
}
int main() {
constexpr size_t N = 1'000'000;
std::vector<int> data(N);
// 生成 0-255 的随机数据
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(0, 255);
for (auto& x : data) x = dist(gen);
// 测试未排序场景
auto t1 = std::chrono::high_resolution_clock::now();
volatile auto s1 = sumIfUnsorted(data);
auto t2 = std::chrono::high_resolution_clock::now();
// 排序后测试
std::sort(data.begin(), data.end());
auto t3 = std::chrono::high_resolution_clock::now();
volatile auto s2 = sumIfSorted(data);
auto t4 = std::chrono::high_resolution_clock::now();
// 分支消除版本
auto t5 = std::chrono::high_resolution_clock::now();
volatile auto s3 = sumBranchless(data);
auto t6 = std::chrono::high_resolution_clock::now();
using ms = std::chrono::milliseconds;
std::cout << "Unsorted: " << duration_cast<ms>(t2 - t1).count() << " ms\n";
std::cout << "Sorted: " << duration_cast<ms>(t4 - t3).count() << " ms\n";
std::cout << "Branchless: " << duration_cast<ms>(t6 - t5).count() << " ms\n";
return 0;
}
典型输出:
Unsorted: 22 ms
Sorted: 7 ms
Branchless: 8 ms
排序后约 3 倍加速,branchless 与排序后接近,且无需排序成本。
示例 4:SIMD 向量化
// 文件: simd_vectorize.cpp
// 演示标量、自动向量化、手写 AVX2 三种实现
// 编译: g++ -O3 -mavx2 -std=c++17 simd_vectorize.cpp -o simd_vectorize
#include <chrono>
#include <immintrin.h>
#include <iostream>
#include <vector>
// 标量版本
// 编译器可能自动向量化,但效果依赖实现
void addScalar(const float* a, const float* b, float* c, size_t n) {
for (size_t i = 0; i < n; ++i) {
c[i] = a[i] + b[i];
}
}
// 手写 AVX2 版本
// 一次处理 8 个 float,显式控制向量化
void addAVX2(const float* a, const float* b, float* c, size_t n) {
size_t i = 0;
// 主循环:每次处理 8 个元素
for (; i + 8 <= n; i += 8) {
__m256 va = _mm256_loadu_ps(a + i); // 加载 8 个 float
__m256 vb = _mm256_loadu_ps(b + i);
__m256 vc = _mm256_add_ps(va, vb); // 向量加法
_mm256_storeu_ps(c + i, vc); // 存储 8 个 float
}
// 处理尾部不足 8 个的元素
for (; i < n; ++i) {
c[i] = a[i] + b[i];
}
}
int main() {
constexpr size_t N = 1 << 20; // 1M 元素
std::vector<float> a(N, 1.0f), b(N, 2.0f), c1(N), c2(N);
auto bench = [](auto func, const char* name) {
auto t1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100; ++i) func();
auto t2 = std::chrono::high_resolution_clock::now();
auto us = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
std::cout << name << ": " << us / 100.0 << " us/iter\n";
};
bench([&] { addScalar(a.data(), b.data(), c1.data(), N); }, "Scalar ");
bench([&] { addAVX2(a.data(), b.data(), c2.data(), N); }, "AVX2 ");
return 0;
}
典型输出:
Scalar : 380 us/iter
AVX2 : 95 us/iter
AVX2 加速约 4 倍(理论 8 倍,受加载/存储开销影响)。
示例 5:内存池避免堆分配开销
// 文件: memory_pool.cpp
// 演示使用对象池替代频繁堆分配
// 编译: g++ -O2 -std=c++17 memory_pool.cpp -o memory_pool
#include <chrono>
#include <iostream>
#include <memory>
#include <vector>
// 简化版对象池
// 预分配一块连续内存,通过链表管理空闲块
// 避免每次 new/delete 调用 malloc/free 的开销
template <typename T, size_t PoolSize>
class ObjectPool {
public:
ObjectPool() {
pool_.reserve(PoolSize);
freeList_.reserve(PoolSize);
for (size_t i = 0; i < PoolSize; ++i) {
freeList_.push_back(i);
pool_.emplace_back();
}
}
// 分配对象:从空闲链表取一个槽位
// 返回指针,所有权归调用方
T* allocate() {
if (freeList_.empty()) return nullptr;
size_t idx = freeList_.back();
freeList_.pop_back();
return &pool_[idx];
}
// 释放对象:归还到空闲链表
void deallocate(T* obj) {
if (obj >= &pool_[0] && obj <= &pool_[PoolSize - 1]) {
size_t idx = obj - &pool_[0];
freeList_.push_back(idx);
}
}
private:
std::vector<T> pool_; // 预分配的对象存储
std::vector<size_t> freeList_; // 空闲槽位索引
};
struct Particle {
float x, y, z;
float vx, vy, vz;
int id;
};
int main() {
constexpr int N = 1'000'000;
// 测试原生 new/delete
auto t1 = std::chrono::high_resolution_clock::now();
{
std::vector<Particle*> particles;
particles.reserve(N);
for (int i = 0; i < N; ++i) {
particles.push_back(new Particle{0, 0, 0, 0, 0, 0, i});
}
for (auto p : particles) delete p;
}
auto t2 = std::chrono::high_resolution_clock::now();
// 测试对象池
auto t3 = std::chrono::high_resolution_clock::now();
{
ObjectPool<Particle, 1'000'000> pool;
std::vector<Particle*> particles;
particles.reserve(N);
for (int i = 0; i < N; ++i) {
auto* p = pool.allocate();
*p = Particle{0, 0, 0, 0, 0, 0, i};
particles.push_back(p);
}
for (auto p : particles) pool.deallocate(p);
}
auto t4 = std::chrono::high_resolution_clock::now();
using ms = std::chrono::milliseconds;
std::cout << "new/delete: " << duration_cast<ms>(t2 - t1).count() << " ms\n";
std::cout << "ObjectPool: " << duration_cast<ms>(t4 - t3).count() << " ms\n";
return 0;
}
典型输出:
new/delete: 85 ms
ObjectPool: 12 ms
对象池加速约 7 倍,主要节省了 malloc/free 与缓存不友好访问的开销。
示例 6:小字符串优化(SSO)的验证
// 文件: sso_check.cpp
// 验证 std::string 的 SSO 行为
// 编译: g++ -O2 -std=c++17 sso_check.cpp -o sso_check
#include <iostream>
#include <string>
int main() {
std::string short_str = "hi"; // 适合 SSO
std::string long_str(50, 'x'); // 超过 SSO 阈值
std::cout << "sizeof(std::string) = " << sizeof(std::string) << "\n";
std::cout << "short_str address: " << static_cast<const void*>(short_str.data()) << "\n";
std::cout << "short_str stack addr: " << static_cast<const void*>(&short_str) << "\n";
std::cout << "long_str data address: " << static_cast<const void*>(long_str.data()) << "\n";
std::cout << "long_str stack addr: " << static_cast<const void*>(&long_str) << "\n";
// 若 short_str.data() 与 &short_str 接近,则数据存储在对象内(SSO)
// 若 long_str.data() 与 &long_str 差异大,则数据存储在堆上
return 0;
}
示例 7:使用 perf 与 Google Benchmark
// 文件: benchmark_example.cpp
// 使用 Google Benchmark 进行微基准测试
// 编译: g++ -O2 -std=c++17 benchmark_example.cpp -lbenchmark -lpthread -o benchmark_example
#include <benchmark/benchmark.h>
#include <vector>
// 基准:遍历 vector 求和
static void BM_VectorSum(benchmark::State& state) {
std::vector<int> data(state.range(0), 1);
for (auto _ : state) {
long long sum = 0;
for (auto x : data) sum += x;
benchmark::DoNotOptimize(sum); // 防止编译器消除
}
state.SetBytesProcessed(state.iterations() * state.range(0) * sizeof(int));
}
BENCHMARK(BM_VectorSum)->Range(1 << 10, 1 << 24);
// 基准:对比 list 与 vector 的遍历
static void BM_ListVsVector(benchmark::State& state) {
if (state.range(0) == 0) {
std::vector<int> data(1 << 20, 1);
for (auto _ : state) {
long long sum = 0;
for (auto x : data) sum += x;
benchmark::DoNotOptimize(sum);
}
} else {
std::list<int> data(1 << 20, 1);
for (auto _ : state) {
long long sum = 0;
for (auto x : data) sum += x;
benchmark::DoNotOptimize(sum);
}
}
}
BENCHMARK(BM_ListVsVector)->Arg(0)->Arg(1);
BENCHMARK_MAIN();
使用 perf 采集热点:
# 采集 CPU 周期热点
perf record -F 99 -g -- ./benchmark_example
# 查看火焰图
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
# 查看缓存未命中
perf stat -e cache-misses,cache-references,L1-dcache-load-misses ./benchmark_example
对比分析
不同优化层次的对比
| 优化层次 | 加速倍数范围 | 实施成本 | 维护成本 | 适用场景 |
|---|---|---|---|---|
| 算法复杂度优化 | 10-1000 倍 | 高 | 低 | 数据规模大的瓶颈算法 |
| 数据结构优化 | 2-50 倍 | 中 | 低 | 频繁查找/插入/删除 |
| 缓存友好重构 | 2-10 倍 | 中 | 中 | 内存密集型计算 |
| 向量化(SIMD) | 2-8 倍 | 中 | 高 | 数据并行计算 |
| 多线程并行 | N 倍(N=核数) | 高 | 高 | CPU 密集型可并行任务 |
| 编译器选项(-O3) | 1.2-2 倍 | 极低 | 极低 | 通用 |
| 内联与模板特化 | 1.1-2 倍 | 低 | 低 | 热点函数调用 |
| 分支预测优化 | 1.5-3 倍 | 低 | 中 | 数据依赖分支 |
| 内存对齐 | 1.1-1.5 倍 | 低 | 低 | SIMD 与缓存敏感场景 |
| 无锁数据结构 | 1.5-5 倍 | 高 | 高 | 低争用高并发场景 |
| NUMA 感知 | 1.5-3 倍 | 中 | 中 | 多插槽服务器 |
| 内联汇编 | 1.5-3 倍 | 极高 | 极高 | 极端性能需求 |
算法复杂度 vs 微优化
算法优化的优势
- 改变渐近复杂度(),在大规模数据上收益巨大
- 一次优化,长期受益,不受硬件变化影响
- 易于理解和维护(若算法本身经典)
微优化的优势
- 在算法已最优时进一步提升常数因子
- 针对特定硬件特性定制
- 不改变代码结构
工程权衡建议
| 情景 | 推荐策略 |
|---|---|
| 数据规模小(< 1000) | 优先微优化,常数因子主导 |
| 数据规模大(> 1M) | 优先算法优化,渐近复杂度主导 |
| 算法已最优 | 微优化是唯一路径 |
| 团队不熟悉算法 | 先评估第三方库,避免重复造轮子 |
| 跨平台需求 | 避免过度依赖特定硬件指令 |
Google Benchmark 与 Catch2 Benchmark 对比
| 维度 | Google Benchmark | Catch2 Benchmark |
|---|---|---|
| 库类型 | 专门基准测试库 | 测试框架附带基准功能 |
| 集成复杂度 | 中(需链接库) | 低(单头文件) |
| 统计指标 | 丰富(均值、中位数、标准差、p值) | 基础(均值、标准差) |
| 输出格式 | JSON、CSV、Console | Console、XML |
| 自动范围 | 支持 Range() | 需手动编写 |
| 适用场景 | 专业性能基准 | 小规模基准,与测试混合使用 |
LTO 与 PGO 对比
| 维度 | LTO (Link-Time Optimization) | PGO (Profile-Guided Optimization) |
|---|---|---|
| 工作原理 | 链接时跨模块内联与优化 | 基于运行时 profile 调整分支预测等 |
| 编译成本 | 链接阶段显著增加 | 需两次构建+训练运行 |
| 优化深度 | 跨模块代码级 | 行为感知,基于真实热点 |
| 典型收益 | 5%-15% | 5%-20% |
| 维护成本 | 低 | 中(profile 需更新) |
| 适用项目 | 中大型项目 | 行为稳定的中大型项目 |
不同内存分配策略对比
| 策略 | 分配延迟 | 内存碎片 | 缓存友好 | 适用场景 |
|---|---|---|---|---|
| malloc/free | 高 | 高 | 差 | 通用 |
| new/delete | 高 | 高 | 差 | C++ 默认 |
| jemalloc | 中 | 低 | 中 | 多线程服务器 |
| tcmalloc | 中 | 低 | 中 | 多线程服务器 |
| mimalloc | 低 | 低 | 高 | 现代(微软) |
| 对象池 | 极低 | 无 | 极高 | 同类型大量分配 |
| Arena 分配器 | 极低 | 中 | 高 | 同生命周期批量分配 |
| Stack 分配器 | 极低 | 无 | 极高 | 临时对象 |
SIMD 各指令集对比
| 指令集 | 向量宽度 | 数据类型 | 寄存器数 | 引入年份 |
|---|---|---|---|---|
| SSE2 | 128 bit | int/float/double | 16 | 2001 |
| AVX | 256 bit | float/double | 16 | 2011 |
| AVX2 | 256 bit | int/float/double | 16 | 2013 |
| AVX-512 | 512 bit | int/float/double | 32 | 2016 |
| SVE | 可变 | 通用 | 32 | 2016(ARM) |
| NEON | 128 bit | int/float | 32 | 2009(ARM) |
常见陷阱与反模式
反模式 1:过早优化
事故案例:某团队在项目初期对每个函数都做了”极致优化”,使用复杂模板元编程与内联汇编。半年后需求变化,修改成本是普通代码的 5 倍,且优化效果在最终架构下不再显著。
根因分析:Knuth 的”过早优化是万恶之源”并非反对优化,而是反对未经验证的优化。在未定位瓶颈前盲目优化,既增加维护成本,又可能优化错误目标。
正确做法:遵循”测量-分析-优化”循环,仅在 profile 验证的热点处优化。
反模式 2:微基准偏差
事故案例:某工程师用 Google Benchmark 测试一个函数,结果显示 5 倍加速。上线后发现整体性能仅提升 2%。事后分析发现:微基准中数据全部命中 L1,真实场景下数据来自主存,差距大幅缩小。
根因分析:微基准容易陷入以下陷阱:
- 数据规模过小,全部在缓存中
- 编译器消除死代码
- 缺乏
benchmark::DoNotOptimize - 单次调用包含冷启动开销
- 数据分布过于规律,触发特殊优化
正确做法:
- 数据规模覆盖从 L1 到主存多个层级
- 使用
DoNotOptimize防止消除 - 多次预热后再测量
- 测试不同输入分布
反模式 3:错误使用 volatile
事故案例:某工程师用 volatile 修饰变量,认为可保证多线程可见性。上线后偶发数据竞争崩溃。
根因分析:volatile 在 C++ 中仅防止编译器优化对该变量的访问,不保证:
- 原子性(读改写仍可能被打断)
- 内存序(其他变量可见性不保证)
- 跨核心可见性(硬件层缓存一致性不保证)
正确做法:多线程同步必须使用 std::atomic 与适当的内存序。
反模式 4:滥用 std::endl
事故案例:某日志库高频输出 std::endl,每次强制 flush 导致 I/O 阻塞,程序性能下降 60%。
根因分析:std::endl 等价于 \n + flush,flush 操作触发系统调用,代价高昂。
正确做法:除调试需要外,使用 \n 替代 std::endl。
反模式 5:容器误选
事故案例:某搜索引擎索引模块使用 std::list 存储文档列表,后期发现遍历性能远低于 std::vector。
根因分析:std::list 节点分散,缓存不友好;std::vector 连续存储,缓存友好。即使 std::list 在 插入删除上有理论优势,实际遍历性能远低于 std::vector。
正确做法:默认选择 std::vector,仅在频繁中间插入删除且不需要遍历时考虑 std::list。Bjarne Stroustrup 在 CppCon 演讲中多次强调此原则。
反模式 6:错误的内联策略
事故案例:某团队将所有小函数都标记 inline,代码膨胀导致 I-cache 未命中上升,性能反而下降 10%。
根因分析:inline 仅是建议,过度内联导致:
- 代码段膨胀,I-cache 压力上升
- 编译时间显著增加
- 二进制体积膨胀
正确做法:让编译器决定,使用 LTO 进行跨模块内联。仅在 profile 显示内联收益时手动控制。
反模式 7:异常在热路径
事故案例:某高频交易系统使用异常处理行情异常,异常抛出开销导致单笔交易延迟超过 100 微秒,远超 SLA。
根因分析:C++ 异常机制在抛出时需栈展开、查找 catch handler,开销达微秒级。Clang 的 libc++ 与 Itanium ABI 尤其明显。
正确做法:
- 热路径使用返回值或
std::expected(C++23) - 异常仅用于真正的异常情况,不用于流程控制
- 极端场景使用
-fno-exceptions关闭异常
反模式 8:误用 std::async
事故案例:某工程师用 std::async(std::launch::async, ...) 大量并发任务,期望自动并行,结果出现死锁与资源耗尽。
根因分析:
std::async返回的 future 析构会阻塞,导致隐式同步- 默认策略
std::launch::async | std::launch::deferred可能退化为延迟执行 - 没有线程池,每个任务可能创建新线程
正确做法:使用专门的线程池(如 boost::asio::thread_pool、TBB、HPX)替代 std::async。
反模式 9:依赖编译器优化
事故案例:某团队假设 -O3 会自动向量化关键循环,在 ARM 平台发现性能远低于预期。
根因分析:编译器自动向量化能力有限,受数据依赖、对齐、别名分析等影响,常常无法向量化看似简单的循环。
正确做法:
- 用编译器报告(
-fopt-info-vec)验证向量化 - 必要时使用 pragma(
#pragma omp simd、#pragma GCC ivdep) - 关键路径手写 SIMD intrinsics 或使用
std::simd(C++26)
反模式 10:虚假共享
事故案例:某多线程计数器数组,每个线程更新自己槽位,性能远低于预期。
struct Counter { int value; }; // 4 字节
Counter counters[8]; // 32 字节,共享缓存行
根因分析:多核 CPU 缓存一致性以缓存行为单位(64 字节),即使各线程访问不同字段,只要在同一缓存行,仍会触发失效与重新加载。
正确做法:使用 alignas(64) 强制每个计数器独占缓存行:
struct alignas(64) Counter { int value; };
Counter counters[8];
工程实践
性能优化工作流
生产级性能优化应遵循以下七步工作流:
- 基线建立:在标准环境采集性能基线,记录 p50/p95/p99 指标
- 瓶颈定位:用 profiler 识别热点,优先关注占用 CPU 时间 >5% 的函数
- 假设建立:基于 profile 数据,提出优化假设与预期收益
- 小规模实验:在微基准中验证假设,排除偏差
- 集成验证:在真实工作负载验证,关注尾延迟与边界场景
- 回归测试:确保功能正确,无性能回退
- 文档化:记录优化前后指标、根因、方案,纳入知识库
性能指标体系
完整性能指标应包含以下维度:
| 维度 | 指标 | 测量方法 |
|---|---|---|
| 吞吐量 | QPS、TPS、FLOPS | 单位时间完成请求数 |
| 延迟 | p50/p95/p99/p999 | 直方图统计 |
| 资源利用率 | CPU、内存、I/O、网络 | 系统监控 |
| 公平性 | 请求延迟方差 | 统计分布 |
| 可扩展性 | 多核/多机加速比 | 不同规模测试 |
| 长尾稳定性 | 24 小时延迟抖动 | 持续监控 |
| 内存占用 | RSS、VMS、堆/栈/共享 | /proc/[pid]/status |
| 缓存命中率 | L1/L2/L3 hit rate | perf stat |
CI 集成的性能回归
# .github/workflows/perf.yml (示意)
name: Performance Regression
on: [pull_request]
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
- name: Run benchmark
run: ./build/benchmark --benchmark_format=json > result.json
- name: Compare with baseline
uses: benchmark-action/github-action-benchmark@v1
with:
tool: 'googlecpp'
output-file-path: result.json
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
alert-threshold: '110%' # 退化 10% 报警
comment-on-alert: true
缓存友好数据结构设计
SOA vs AOS
// AOS (Array of Structs):传统设计
struct ParticleAOS {
float x, y, z; // 12 字节
float vx, vy, vz; // 12 字节
float mass; // 4 字节
int id; // 4 字节
};
std::vector<ParticleAOS> particles_aos;
// 仅计算 x 时,缓存行加载了大量无用字段(vx, vy, vz, mass, id)
// SOA (Struct of Arrays):向量化友好
struct ParticleSOA {
std::vector<float> x, y, z;
std::vector<float> vx, vy, vz;
std::vector<float> mass;
std::vector<int> id;
};
// 仅计算 x 时,缓存行全部是 x 数据,SIMD 加载效率最大化
SOA 在 SIMD 与缓存利用率上通常优于 AOS 2-5 倍,但代码可读性下降。
分离热冷数据
// 优化前:热数据与冷数据混合
struct Entity {
Vec3 position; // 热:每帧更新
Vec3 velocity; // 热:每帧更新
std::string name; // 冷:仅 UI 显示
Texture* texture; // 冷:仅渲染时使用
Stats stats; // 冷:仅统计时使用
};
// 优化后:热冷分离
struct EntityHot {
Vec3 position;
Vec3 velocity;
size_t cold_index; // 指向冷数据
};
struct EntityCold {
std::string name;
Texture* texture;
Stats stats;
};
std::vector<EntityHot> hot_data; // 紧凑,缓存友好
std::vector<EntityCold> cold_data; // 仅需要时访问
热冷分离后,主循环遍历 hot_data 时缓存命中率显著提升,常见加速 2-4 倍。
编译选项优化
# 高性能场景推荐编译选项
g++ -O3 \
-march=native \ # 启用本机所有指令集
-mtune=native \ # 针对本机微架构调优
-flto \ # 链接时优化
-fprofile-use \ # PGO:使用 profile
-fdevirtualize \ # 虚函数去虚化
-fipa-pta \ # 过程间指针分析
-fgraphite \ # 多面体优化(循环变换)
-funroll-loops \ # 循环展开
-fvect-cost-model=unlimited \ # 激进向量化
-fopt-info-vec-optimized=vec.log \ # 向量化报告
-DNDEBUG \ # 关闭 assert
-std=c++17 main.cpp
内存分配器选择
// 使用 jemalloc/tcmalloc 替换默认 malloc
// 链接时指定:-ljemalloc
#include <jemalloc/jemalloc.h>
// 或者通过 LD_PRELOAD 注入(无需重编译)
// LD_PRELOAD=/usr/lib/libjemalloc.so ./app
// C++17 起可使用 aligned_alloc 控制对齐
#include <cstdlib>
void* ptr = std::aligned_alloc(64, 1024); // 64 字节对齐,1KB
std::free(ptr);
// 使用 polymorphic memory resources (PMR) 控制分配策略
#include <memory_resource>
std::pmr::monotonic_buffer_resource mbr{buffer, sizeof(buffer)};
std::pmr::vector<int> vec{&mbr}; // 使用预分配 buffer,无堆分配
内联与函数属性
// 强烈建议内联(编译器可忽略)
inline int fastAdd(int a, int b) { return a + b; }
// C++17 强制内联(若失败则编译错误,适用于关键路径)
constexpr __attribute__((always_inline)) int criticalAdd(int a, int b) {
return a + b;
}
// 标记热点函数(GCC/Clang 扩展)
__attribute__((hot)) void frequentlyCalled() { /* ... */ }
// 标记冷函数(优化代码布局)
__attribute__((cold)) void errorHandler() { /* ... */ }
// 分支提示(C++20 标准)
if constexpr (true) [[likely]] { /* hot path */ }
if (error) [[unlikely]] { /* cold path */ }
异常与 RTTI 关闭
在极端性能场景(高频交易、游戏引擎)关闭异常与 RTTI 可减少二进制体积与某些隐藏开销:
# 关闭异常与 RTTI
g++ -fno-exceptions -fno-rtti -O3 main.cpp
代价是无法使用 dynamic_cast、typeid、throw。需要替代方案:
// 替代 dynamic_cast:CRTP 或访问者模式
template <typename Derived>
class Base {
public:
void method() { static_cast<Derived*>(this)->implMethod(); }
};
// 替代异常:返回值或 std::expected
std::expected<int, ErrorCode> parse(const std::string& s);
案例研究
案例 1:LLVM 编译器优化 Pass 的性能改进
LLVM 项目在 2020 年对 InstCombine Pass 进行了重构,通过以下手段将编译性能提升约 15%:
- 数据结构改造:将
SmallVector默认容量从 4 调整为 8,减少堆分配 - 循环优化:对热点循环进行循环展开与向量化
- 分支减少:用查表替代 if-else 链
- 内存布局:将
Instruction类的常用字段前置,提升缓存命中率 - LTO:启用 LTO 进行跨模块内联
详细数据见 LLVM 论文 Scaling the LLVM Pass Manager (LLVM Dev Meeting 2020)。
案例 2:Meta Folly 库的 fbstring 设计
Meta(原 Facebook)的 Folly 库提供了 fbstring,作为 std::string 的高性能替代。其核心设计:
- 三方存储策略:
- 小字符串(<23 字节,64 位):内联存储(SSO),无堆分配
- 中等字符串(23-255 字节):堆分配,引用计数共享
- 大字符串(>=256 字节):使用
mmap,避免占用物理内存
- 异构查找:
find()使用memchr等 SIMD 优化函数 - 原子引用计数:多线程安全的拷贝
实测在中短字符串场景比 std::string 快 2-3 倍,在长字符串场景节省 30% 内存。
案例 3:Google Abseil 的 absl::flat_hash_map
Google 推出 absl::flat_hash_map 替代 std::unordered_map,主要优化:
- 开放寻址法:替代
std::unordered_map的链地址法,消除指针追逐 - 元数据内联:每个桶附带 1 字节元数据,缓存友好
- SIMD 查找:使用 SSE2/AVX2 一次比较 16/32 个桶
- 缓存友好布局:桶与元数据连续存储
实测在小 key 场景比 std::unordered_map 快 2-5 倍,内存占用减少 50%-70%。
案例 4:高频交易系统纳秒级延迟优化
某高频交易团队从微秒级优化到纳秒级的关键步骤:
- 架构:核心路径无系统调用,纯用户态;无锁数据结构
- 编译:
-O3 -march=native -fno-exceptions -fno-rtti,启用 PGO - 内存:预分配 + 内存池,NUMA 绑定
- CPU:
taskset绑核,关闭超线程,关闭节能模式 - 网络:DPDK 内核旁路,Solarflare 网卡 + Onload
- 数据结构:平面数组替代树,定制 lock-free 队列
- 测量:DPDK TSC 计数器,纳秒级精度
最终从 5 微秒延迟优化到 200 纳秒,p99 < 500 纳秒。
案例 5:游戏引擎 ECS 重构
某 3A 游戏引擎从传统 OOP 重构为 ECS(Entity Component System)架构,关键改进:
- 数据导向设计:组件按类型连续存储(SOA)
- 批处理:每帧按系统批量处理所有实体
- 缓存命中率:从 35% 提升到 78%
- 多线程并行:系统间无依赖可并行执行
帧率从 45 FPS 提升到 120 FPS,且 CPU 利用率更均衡。
案例 6:Chromium V8 JavaScript 引擎优化
V8 引擎在 2017 年引入 TurboFan 优化编译器,关键改进:
- 基于 IR 的优化:替代 Crankshaft 的图式 IR 更强大
- 逃逸分析:栈分配替代堆分配
- 内联缓存:加速方法分派
- 并发编译:不阻塞主线程
Octane 基准分数提升约 30%,真实网页场景提升 10%-15%。
习题
基础题
题 1: 解释 Amdahl 定律与 Gustafson 定律的区别,各适用于何种场景?
参考答案要点:
- Amdahl:固定问题规模,加速比受串行部分上界;适用于桌面/嵌入式等固定负载
- Gustafson:问题规模随处理器数扩展;适用于 HPC、批处理等可扩展问题
- 二者并非矛盾,而是不同假设下的结论
题 2: 缓存未命中代价如何量化?给出一个估算公式。
参考答案要点:
其中 为未命中次数, 为主存延迟, 为命中次数, 为缓存延迟。
题 3: 列举 3 种避免分支预测失败的方法。
参考答案要点:
- 数据排序后处理,使分支模式可预测
- 用位运算替代条件分支(branchless)
- 用查找表替代 if-else 链
- 使用
[[likely]]/[[unlikely]]提示编译器
进阶题
题 4: 给定以下代码,分析其性能瓶颈并提出 3 种优化方案。
void process(std::list<int>& data) {
for (auto it = data.begin(); it != data.end(); ++it) {
if (*it < 0) {
data.erase(it);
}
}
}
参考答案要点:
瓶颈:
std::list节点分散,缓存不友好erase后迭代器失效(实际代码有 bug)- 频繁分支
优化方案:
- 改用
std::vector<int>,配合std::remove_if+erase惯用法 - 使用 branchless 谓词
- 批量处理,先标记后删除
题 5: 解释 SOA 比 AOS 在 SIMD 场景下更优的原因,并指出 SOA 的代价。
参考答案要点:
- SOA 优势:同类型数据连续存储,SIMD 加载连续 8/16 个元素高效
- SOA 优势:缓存利用率高,仅加载需要的数据
- SOA 代价:代码可读性下降,跨字段操作复杂
- SOA 代价:对象生命周期管理复杂
题 6: 解释为什么 std::async 不适合大量并发任务,并给出替代方案。
参考答案要点:
std::async没有线程池,可能每任务创建线程- 返回的 future 析构阻塞,导致隐式同步
- 默认策略可能退化为延迟执行
- 替代:
boost::asio::thread_pool、Intel TBB、HPX、自建线程池
挑战题
题 7: 设计一个高性能的线程安全计数器,要求在 32 核服务器上每秒可执行 1 亿次自增。
参考答案要点:
- 朴素
std::atomic<int>在高争用下退化为序列化 - 方案 1:分片计数器,每核独立计数,读取时合并
- 方案 2:NUMA 感知分片,每 NUMA 节点独立
- 方案 3:
std::atomic_ref+ 缓存行对齐
示例代码:
struct alignas(64) Shard { std::atomic<long> value{0}; };
std::array<Shard, 32> shards;
void increment(int thread_id) {
shards[thread_id].value.fetch_add(1, std::memory_order_relaxed);
}
long read() {
long sum = 0;
for (auto& s : shards) sum += s.value.load(std::memory_order_relaxed);
return sum;
}
题 8: 分析以下代码在 ARM 与 x86 上行为的差异,并解释原因。
std::atomic<int> x{0};
std::atomic<bool> ready{false};
// Thread 1
x.store(42, std::memory_order_relaxed);
ready.store(true, std::memory_order_release);
// Thread 2
while (!ready.load(std::memory_order_acquire));
assert(x.load(std::memory_order_relaxed) == 42); // 必然成立
参考答案要点:
- 在 x86(TSO 模型)上,store 顺序天然不被重排,即使
relaxed也保证可见性 - 在 ARM(弱内存模型)上,
relaxedstore 可能被重排,但release保证其前的 store 在releasestore 前完成 acquire保证其后的 load 不被重排到acquireload 前- 因此 acquire-release 配对保证 Thread 2 看到 x=42,跨架构成立
- 若改用
relaxed一致,ARM 上 assert 可能失败
题 9: 阅读以下代码,识别性能问题并给出优化版本。
std::vector<std::string> loadFiles(const std::vector<std::string>& paths) {
std::vector<std::string> contents;
for (const auto& path : paths) {
std::ifstream file(path);
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
contents.push_back(content); // 触发拷贝
}
return contents;
}
参考答案要点:
问题:
push_back触发std::string拷贝(SSO 外)- 单线程串行 I/O
- 未使用
reserve预分配
优化版本:
std::vector<std::string> loadFiles(const std::vector<std::string>& paths) {
std::vector<std::string> contents;
contents.reserve(paths.size()); // 预分配
// 并行读取
std::vector<std::future<std::string>> futures;
futures.reserve(paths.size());
for (const auto& path : paths) {
futures.push_back(std::async(std::launch::async, [&path]() {
std::ifstream file(path, std::ios::binary | std::ios::ate);
auto size = file.tellg();
file.seekg(0);
std::string content;
content.resize(size);
file.read(content.data(), size); // 直接读入目标 string
return content;
}));
}
for (auto& f : futures) {
contents.push_back(std::move(f.get())); // 移动而非拷贝
}
return contents;
}
题 10: 论述在以下场景中,你会选择何种优化策略,并说明理由:
- 场景 A:嵌入式设备(512KB RAM)上的音频解码器
- 场景 B:云服务器上的机器学习推理服务
- 场景 C:游戏引擎的物理模拟模块
参考答案要点:
- A:内存占用优先,使用定点运算、SIMD(若支持)、查表替代除法、避免动态分配
- B:吞吐量与延迟并重,使用 SIMD(BLAS)、批处理、线程池、NUMA 优化、量化推理
- C:帧率优先,使用 SIMD(SSE/AVX)、数据导向设计(ECS)、缓存友好布局、SIMD 数学库
参考文献
[1] Stroustrup, B. 2013. The C++ Programming Language, 4th edition. Addison-Wesley Professional. ISBN: 978-0321563842. DOI: https://doi.org/10.5555/2502040
[2] Meyers, S. 2004. Effective C++: 55 Specific Ways to Improve Your Programs and Designs, 3rd edition. Addison-Wesley Professional. ISBN: 978-0321334879.
[3] Fog, A. 2024. Optimizing software in C++: An optimization guide for Windows, Linux and macOS platforms. Technical University of Denmark. Available: https://www.agner.org/optimize/optimizing_cpp.pdf
[4] Drepper, U. 2007. What Every Programmer Should Know About Memory. Red Hat, Inc. Available: https://people.freebsd.org/~lstewart/articles/cpumemory.pdf
[5] Sutter, H. 2005. The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software. Dr. Dobb’s Journal, 30(3). Available: https://www.gotw.ca/publications/concurrency-ddj.htm
[6] Hennessy, J. L. and Patterson, D. A. 2017. Computer Architecture: A Quantitative Approach, 6th edition. Morgan Kaufmann. ISBN: 978-0128119051. DOI: https://doi.org/10.1016/C2015-0-06063-5
[7] Amdahl, G. M. 1967. Validity of the single processor approach to achieving large scale computing capabilities. In Proceedings of the AFIPS ‘67 Spring Joint Computer Conference (AFIPS ‘67), 483-485. DOI: https://doi.org/10.1145/1465482.1465560
[8] Gustafson, J. L. 1988. Reevaluating Amdahl’s law. Communications of the ACM, 31(5), 532-533. DOI: https://doi.org/10.1145/42411.42415
[9] Williams, S., Waterman, A., and Patterson, D. 2009. Roofline: an insightful visual performance model for multicore architectures. Communications of the ACM, 52(4), 65-76. DOI: https://doi.org/10.1145/1498765.1498785
[10] Hoare, C. A. R. 1978. Communicating Sequential Processes. Communications of the ACM, 21(8), 666-677. DOI: https://doi.org/10.1145/359576.359585
[11] Leiserson, C. E., Thompson, N. C., Emer, J. S., Kwasniewski, G. P., Lampson, J. W., Sanchez, D., and Schardl, T. B. 2020. There’s plenty of room at the Top: What will drive computer performance after Moore’s law ends? Science, 370(6523). DOI: https://doi.org/10.1126/science.aba0984
[12] Patterson, D. A. and Hennessy, J. L. 2020. Computer Organization and Design RISC-V Edition: The Hardware Software Interface, 2nd edition. Morgan Kaufmann. ISBN: 978-0128203316.
[13] Schmidt, D. C. and Huston, S. D. 2012. C++ Network Programming: Systematic Reuse with ACE and Frameworks, Volume 2. Addison-Wesley Professional. ISBN: 978-0201795254.
[14] Josuttis, N. M. 2012. The C++ Standard Library: A Tutorial and Reference, 2nd edition. Addison-Wesley Professional. ISBN: 978-0321623218.
[15] ISO/IEC 14882:2023. Information technology — Programming languages — C++. International Organization for Standardization. Available: https://www.iso.org/standard/83626.html
[16] McKinley, K. S., Carr, S., and Tseng, C.-W. 1996. Improving data locality with loop transformations. ACM Transactions on Programming Languages and Systems (TOPLAS), 18(4), 424-453. DOI: https://doi.org/10.1145/233561.233563
[17] Carr, S. and Kennedy, K. 1994. Improving the ratio of memory operations to floating-point operations in loops. ACM Transactions on Programming Languages and Systems (TOPLAS), 16(6), 1768-1810. DOI: https://doi.org/10.1145/197320.197329
[18] Click, C. 2005. Azul Systems: Experiences with Hardware-Assisted Garbage Collection. In Proceedings of the 4th International Symposium on Memory Management (ISMM ‘06). DOI: https://doi.org/10.1145/1133956.1133961
延伸阅读
官方文档
- GCC 优化选项: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
- Clang 性能选项: https://clang.llvm.org/docs/UsersManual.html#optimization-options
- perf 工具: https://perf.wiki.kernel.org/
- Intel VTune Profiler: https://www.intel.com/content/www/us/en/develop/documentation/vtune-help/top.html
- Intel Intrinsics Guide: https://www.intel.com/content/www/us/en/docs/intrinsics-guide/
- Google Benchmark: https://github.com/google/benchmark
- Tracy Profiler: https://github.com/wolfpld/tracy
经典教材
- 《计算机系统性能优化》,Bryan O’Sullivan
- 《Computer Architecture: A Quantitative Approach》,John L. Hennessy, David A. Patterson
- 《What Every Programmer Should Know About Memory》,Ulrich Drepper
- 《Optimizing Software in C++》,Agner Fog
- 《Data-Oriented Design》,Richard Fabian
- 《高性能C++》,Google CppCon 演讲合集
- 《Effective Modern C++》,Scott Meyers
- 《C++ Concurrency in Action》,Anthony Williams, 2nd Edition
前沿论文与演讲
- Chandler Carruth, “Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!”, CppCon 2015
- Andrei Alexandrescu, “Fastware”, CppCon 2016
- Scott Meyers, “CPU Caches and Why You Care”, code::dive 2014
- Mike Acton, “Data-Oriented Design and C++”, CppCon 2014
- Emil Dotchevski, “Practical C++ Performance Cases”, CppCon 2018
- Fedor Pikus, “CPU Caches and How They Affect Your Code”, CppCon 2017
- David Abrahams, “Want Fast C++? Know Your Hardware!”, NDC 2019
- Patrick Bronnimann, “Profilers, Optimizers, and What They’re Doing Behind Your Back”, CppNow 2021
开源项目源码
- Folly: https://github.com/facebook/folly
- Abseil: https://github.com/abseil/abseil-cpp
- LLVM: https://github.com/llvm/llvm-project
- EASTL: https://github.com/electronicarts/EASTL
- EnTT: https://github.com/skypjack/entt
- Google Highway: https://github.com/google/highway
性能优化社区资源
- Agner Fog 的 CPU 优化手册: https://www.agner.org/optimize/
- Brendan Gregg 的性能分析工具集: http://www.brendangregg.com/
- Martin Thompson 的机械同情博客: https://mechanical-sympathy.blogspot.com/
- Perf Wiki: https://perf.wiki.kernel.org/
- CppCon Performance Track 历年演讲合集
在线课程
- MIT 6.172 Performance Engineering of Software Systems: https://ocw.mit.edu/courses/6-172-performance-engineering-of-software-systems-fall-2018/
- CMU 15-418 Parallel Computer Architecture and Programming: http://15418.courses.cs.cmu.edu/
- Coursera: Code Optimization and Performance Tuning
- Intel Software College: SIMD Programming