Skip to content

缓存友好与性能设计

性能优化应以 profiler、延迟分位数和真实负载为依据。算法复杂度、分配次数、缓存局部性、分支预测和并发争用往往比某一条语法技巧影响更大。

什么是缓存友好

CPU 通常按缓存行从内存取数据(许多机器为 64 字节,但不是 C++ 保证)。顺序访问连续数据能充分利用一条缓存行;随机指针追逐会增加 cache miss 和内存等待。

缓存友好代码的常见特征:

  • 顺序遍历连续容器(如 std::vector);
  • 让经常一起访问的数据靠近;
  • 减少每元素的动态分配和间接指针;
  • 将高频访问的热数据与低频冷数据分离;
  • 在并发写入时避免伪共享,详见“并发与多线程”。

AoS、SoA 与冷热分离

AoS(Array of Structures)适合每次都使用对象的多数成员:

cpp
struct Particle {
    Vec3 position;
    Vec3 velocity;
    int id;
    std::chrono::system_clock::time_point created_at;
};
std::vector<Particle> particles;

如果物理更新循环只读取和写入 position/velocity,SoA(Structure of Arrays)可避免把 id 和时间戳反复载入缓存:

cpp
struct ParticleStorage {
    std::vector<Vec3> positions;
    std::vector<Vec3> velocities;
    std::vector<int> ids; // 冷数据
    std::vector<std::chrono::system_clock::time_point> created_at;
};

SoA 不总是更好:当经常按单个实体访问全部字段,AoS 更直观也可能更快。选择取决于真实访问模式。alignas 可控制对象起始地址的对齐,但不能自动解决所有缓存问题;padding 也会增加内存占用,先测量再使用。

常见性能设计策略

  1. 先选对算法与数据结构:例如 Top K 常用大小为 K 的堆,而不是完整排序;
  2. 减少分配:已知规模时 reserve,复用缓冲区,避免热路径频繁构造大对象;
  3. 批量处理:合并小 I/O、批量提交、按数组遍历;
  4. 控制间接层级:连续存储通常比链表和散落堆对象更利于缓存;
  5. 避免伪共享:不同线程频繁写的计数器可分开缓存行或先线程本地累积再汇总;
  6. 验证优化结果:在 release 构建、目标硬件和有代表性的输入下比较吞吐、P95/P99 与资源消耗。

性能面试的表达边界

  • std::vector 扩容的增长倍率是实现细节,标准只保证相关复杂度;尾部追加通常是均摊 O(1);
  • 移动语义通常减少资源复制,但不要把“缓存命中率提升”当作它的必然直接效果;
  • 虚函数、std::function、RTTI 都有潜在间接调用/局部性成本,但不代表应在所有地方替换;先判断它们是否处于热点;
  • 多线程不必然更快:同步、调度、缓存一致性和下游饱和都可能使延迟变差。

使用 Markdown 与 VitePress 构建