C++编程实践——提高缓存的命中

一、缓存

缓存其实好理解,在前面分析内存时,包括分析其它优化方式或框架时,都有过详细的说明。缓存可以理解为图书馆找资料,管理员一般会把大家最经常查找的资料放到最显眼的地方。而计算机中的缓存类似,把可能频繁访问的数据放到高速存储形成的缓存中。

但这就带来一个明显的问题,缓存如果没有命中,反而会导致访问时间的增加。所以如何用好缓存,在很多情况下,是一个对性能和效率影响非常关键的问题。

二、缓存影响的因素

缓存出现的原因本质在于计算机访问的局部性,它可能是时间局部性也可能是空间局部性,也有可能是二者都有。时间局部性是指有可能在较短时间内不断访问相同数据而空间局部性是指可能不断访问某一块内存中的数据。

明白了其本质,就可以找到影响缓存的因素:

  1. 硬件方面
    这个相当重要,主要有缓存的大小、级数;缓存块的大小以及硬件延迟的时间;多核中的一致性处理。更详细的技术内容可参考计算机原理及其它相关资料
  2. 软件方面
    硬件的灵魂在软件,软件为支持缓存提供了映射策略、替换算法和写入机制等。具体的策略和内容这里不展开,相关的资料和教科书中太多了。但是本文更关心的是上层软件应用中对缓存的命中影响,如内存、算法等
  3. 综合
    其实在真正的缓存应用中,往往是二者互相配合,实现高效的缓存利用,比如缓存的大小和级数与映射策略和替换算法等协同处理数据

在真实的环境中,不同的平台和不同的应用场景下可能会对缓存的命中率、延迟和功耗以及成本等这间进行综合考量,未必一定是命中率最高就会更好。

三、C++如何提高缓存的命中率

对于C++来说,要想提高缓存的命中率,就需要从语言本身的角度并配合系统和硬件平台有针对性的进行相关的处理。主要包括以下几部分:

  1. 充分利用局部性
    特别是内存访问中,可以使用连续内存来提高缓存的命中率、对数据结构对齐。尽量减少类似链表这种数据结构的使用
  2. 算法的设计匹配
    不同算法的应用就需要匹配不同的数据访问策略,比如在大量只读的场景中和不断的插入、删除场景中,就无法使用相同的缓存策略
  3. 内存的处理
    内存和缓存一般来说是"哥俩"。要想很好的利用缓存,那么是使用栈空间还是堆空间、内存有没有碎片这些都可能影响到缓存的命中率
  4. 循环的处理
    循环的处理有一点类似时间局部性或者说二者结合。在前面的优化中提到过,可以通过循环展开、分块等来实现对缓存的友好
  5. 分支的处理
    分支就容易理解了,前面反复提到的likely等其实就是一种类似于这种机制。另外从设计或编译上消灭分支则是一种更高效的缓存友好的方式
  6. 内联函数
    内联函数其实也是一种局部性的应用,不用在调用函数时进行跳转
  7. 避免过深的递归和嵌套
    这其实也是对局部性的一种体现。只要在可控范围内,递归和嵌套都可以被优化。比如递归可以被尾部调用或迭代优化等等

影响缓存的情况可能千差万别,同样的代码在不同的环境下,可能会产生不同的效果,一定要加以高度重视。切不可教条的生搬硬套。正如前面反复提到的"最合适的就是最好的"。

四、例程

下面看一个例程:

c 复制代码
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <numeric>
#include <random>
#include <stdexcept>
#include <vector>

#if defined(__GNUC__) || defined(__clang__)
#define LIKELY(x) __builtin_expect(!!(x), 1)
#else
#define LIKELY(x) (x)
#endif

template <class T, std::size_t Alignment> class AlignedAllocator {
public:
  using value_type = T;

  AlignedAllocator() noexcept = default;

  template <class U> AlignedAllocator(const AlignedAllocator<U, Alignment> &) noexcept {}

  T *allocate(std::size_t n) {
    if (n == 0) {
      return nullptr;
    }

    void *p = nullptr;
    if (posix_memalign(&p, Alignment, n * sizeof(T)) != 0) {
      throw std::bad_alloc();
    }
    return static_cast<T *>(p);
  }

  void deallocate(T *p, std::size_t) noexcept { std::free(p); }

  template <class U> struct rebind { using other = AlignedAllocator<U, Alignment>; };
};

template <class T, class U, std::size_t Alignment> bool operator==(const AlignedAllocator<T, Alignment> &, const AlignedAllocator<U, Alignment> &) {
  return true;
}

template <class T, class U, std::size_t Alignment> bool operator!=(const AlignedAllocator<T, Alignment> &, const AlignedAllocator<U, Alignment> &) {
  return false;
}

using AlignedFloatVector = std::vector<float, AlignedAllocator<float, 64>>;

// 1. 连续内存 + 对齐:SoA 比 AoS 更适合只读取部分字段的热点循环。
struct Particles {
  AlignedFloatVector x;
  AlignedFloatVector y;
  AlignedFloatVector z;
  AlignedFloatVector vx;
  AlignedFloatVector vy;
  AlignedFloatVector vz;
  AlignedFloatVector mass;
  std::vector<unsigned char> active;

  explicit Particles(std::size_t n) : x(n), y(n), z(n), vx(n), vy(n), vz(n), mass(n), active(n, 1) {}

  std::size_t size() const { return x.size(); }
};

// 6. 内联:热点小函数避免调用开销,也让编译器更容易做向量化和常量传播。
inline float clamp01(float v) { return std::min(1.0f, std::max(0.0f, v)); }

inline void integrate_one(float &x, float &y, float &z, float vx, float vy, float vz, float dt) {
  x += vx * dt;
  y += vy * dt;
  z += vz * dt;
}

void init_particles(Particles &p) {
  std::mt19937 rng(7);
  std::uniform_real_distribution<float> dist(-1.0f, 1.0f);

  for (std::size_t i = 0; i < p.size(); ++i) {
    p.x[i] = dist(rng);
    p.y[i] = dist(rng);
    p.z[i] = dist(rng);
    p.vx[i] = dist(rng) * 0.01f;
    p.vy[i] = dist(rng) * 0.01f;
    p.vz[i] = dist(rng) * 0.01f;
    p.mass[i] = 0.5f + clamp01(std::abs(dist(rng)));
  }
}

void integrate_particles(Particles &p, float dt) {
  const std::size_t n = p.size();

  // 4. 循环展开:减少循环控制开销,提升连续访问吞吐。
  std::size_t i = 0;
  for (; i + 3 < n; i += 4) {
    integrate_one(p.x[i], p.y[i], p.z[i], p.vx[i], p.vy[i], p.vz[i], dt);
    integrate_one(p.x[i + 1], p.y[i + 1], p.z[i + 1], p.vx[i + 1], p.vy[i + 1], p.vz[i + 1], dt);
    integrate_one(p.x[i + 2], p.y[i + 2], p.z[i + 2], p.vx[i + 2], p.vy[i + 2], p.vz[i + 2], dt);
    integrate_one(p.x[i + 3], p.y[i + 3], p.z[i + 3], p.vx[i + 3], p.vy[i + 3], p.vz[i + 3], dt);
  }

  for (; i < n; ++i) {
    integrate_one(p.x[i], p.y[i], p.z[i], p.vx[i], p.vy[i], p.vz[i], dt);
  }
}

float kinetic_energy_read_mostly(const Particles &p) {
  constexpr std::size_t block = 4096;
  float total = 0.0f;

  // 2. 算法匹配:大量只读扫描使用顺序分块,避免随机访问和复杂缓存策略。
  for (std::size_t base = 0; base < p.size(); base += block) {
    const std::size_t end = std::min(base + block, p.size());
    float local = 0.0f;

    for (std::size_t i = base; i < end; ++i) {
      const float speed2 = p.vx[i] * p.vx[i] + p.vy[i] * p.vy[i] + p.vz[i] * p.vz[i];
      local += 0.5f * p.mass[i] * speed2;
    }
    total += local;
  }

  return total;
}

void delete_inactive_and_compact(Particles &p) {
  std::size_t write = 0;

  // 2. 算法匹配:频繁删除不使用链表,先标记,集中 compaction 保持数组紧凑。
  for (std::size_t read = 0; read < p.size(); ++read) {
    if (LIKELY(p.active[read] != 0)) {
      if (write != read) {
        p.x[write] = p.x[read];
        p.y[write] = p.y[read];
        p.z[write] = p.z[read];
        p.vx[write] = p.vx[read];
        p.vy[write] = p.vy[read];
        p.vz[write] = p.vz[read];
        p.mass[write] = p.mass[read];
        p.active[write] = 1;
      }
      ++write;
    }
  }

  p.x.resize(write);
  p.y.resize(write);
  p.z.resize(write);
  p.vx.resize(write);
  p.vy.resize(write);
  p.vz.resize(write);
  p.mass.resize(write);
  p.active.resize(write);
}

void blocked_matmul(const AlignedFloatVector &a, const AlignedFloatVector &b, AlignedFloatVector &c, std::size_t n) {
  constexpr std::size_t block = 32;
  std::fill(c.begin(), c.end(), 0.0f);

  // 4. 分块:让 A/B/C 的工作集尽量留在 L1/L2 cache 中。
  for (std::size_t ii = 0; ii < n; ii += block) {
    for (std::size_t kk = 0; kk < n; kk += block) {
      for (std::size_t jj = 0; jj < n; jj += block) {
        const std::size_t i_end = std::min(ii + block, n);
        const std::size_t k_end = std::min(kk + block, n);
        const std::size_t j_end = std::min(jj + block, n);

        for (std::size_t i = ii; i < i_end; ++i) {
          for (std::size_t k = kk; k < k_end; ++k) {
            const float aik = a[i * n + k];
            std::size_t j = jj;

            for (; j + 3 < j_end; j += 4) {
              c[i * n + j] += aik * b[k * n + j];
              c[i * n + j + 1] += aik * b[k * n + j + 1];
              c[i * n + j + 2] += aik * b[k * n + j + 2];
              c[i * n + j + 3] += aik * b[k * n + j + 3];
            }

            for (; j < j_end; ++j) {
              c[i * n + j] += aik * b[k * n + j];
            }
          }
        }
      }
    }
  }
}

float branchless_sum_positive(const AlignedFloatVector &v) {
  float sum = 0.0f;

  // 5. 分支处理:把 if (x > 0) sum += x 改成 max(x, 0),减少错误预测。
  for (float x : v) {
    sum += std::max(x, 0.0f);
  }

  return sum;
}

struct CsrGraph {
  std::vector<int> offsets;
  std::vector<int> edges;
};

CsrGraph build_line_graph(int n) {
  CsrGraph g;
  g.offsets.resize(static_cast<std::size_t>(n) + 1);
  g.edges.reserve(static_cast<std::size_t>(n - 1) * 2);

  for (int i = 0; i < n; ++i) {
    g.offsets[static_cast<std::size_t>(i)] = static_cast<int>(g.edges.size());
    if (i > 0) {
      g.edges.push_back(i - 1);
    }
    if (i + 1 < n) {
      g.edges.push_back(i + 1);
    }
  }
  g.offsets[static_cast<std::size_t>(n)] = static_cast<int>(g.edges.size());
  return g;
}

int iterative_dfs_count(const CsrGraph &g, int start) {
  const int n = static_cast<int>(g.offsets.size()) - 1;
  std::vector<unsigned char> visited(static_cast<std::size_t>(n), 0);
  std::vector<int> stack;
  stack.reserve(static_cast<std::size_t>(n));

  // 7. 避免深递归:显式栈比递归调用更可控,也减少栈溢出风险。
  stack.push_back(start);
  int count = 0;

  while (!stack.empty()) {
    const int u = stack.back();
    stack.pop_back();

    if (visited[static_cast<std::size_t>(u)]) {
      continue;
    }

    visited[static_cast<std::size_t>(u)] = 1;
    ++count;

    for (int e = g.offsets[static_cast<std::size_t>(u)]; e < g.offsets[static_cast<std::size_t>(u + 1)]; ++e) {
      const int v = g.edges[static_cast<std::size_t>(e)];
      if (!visited[static_cast<std::size_t>(v)]) {
        stack.push_back(v);
      }
    }
  }

  return count;
}

int main() {
  constexpr std::size_t particle_count = 1'000'000;
  Particles particles(particle_count);
  init_particles(particles);

  // 3. 内存处理:小型临时数据放栈上,大块数据一次性分配并复用,减少堆碎片。
  std::array<float, 16> stack_scratch{};
  std::iota(stack_scratch.begin(), stack_scratch.end(), 1.0f);

  const auto t0 = std::chrono::steady_clock::now();
  integrate_particles(particles, 0.016f);
  const float energy = kinetic_energy_read_mostly(particles);

  for (std::size_t i = 0; i < particles.size(); i += 17) {
    particles.active[i] = 0;
  }
  delete_inactive_and_compact(particles);

  constexpr std::size_t n = 128;
  AlignedFloatVector a(n * n);
  AlignedFloatVector b(n * n);
  AlignedFloatVector c(n * n);
  for (std::size_t i = 0; i < n * n; ++i) {
    a[i] = static_cast<float>(static_cast<int>(i % 13) - 6) * 0.01f;
    b[i] = static_cast<float>(static_cast<int>(i % 17) - 8) * 0.01f;
  }
  blocked_matmul(a, b, c, n);

  const float positive_sum = branchless_sum_positive(c);
  const CsrGraph graph = build_line_graph(100000);
  const int reachable = iterative_dfs_count(graph, 0);
  const auto t1 = std::chrono::steady_clock::now();

  const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
  std::cout << "particles after compact: " << particles.size() << '\n';
  std::cout << "kinetic energy: " << energy << '\n';
  std::cout << "matrix positive sum: " << positive_sum << '\n';
  std::cout << "reachable nodes: " << reachable << '\n';
  std::cout << "stack scratch first/last: " << stack_scratch.front() << ", " << stack_scratch.back() << '\n';
  std::cout << "elapsed ms: " << ms << '\n';

  return 0;
}

五、总结

缓存机制无论在底层实现还是在上层应用上,都有着重要的作用。开发者不但要掌握相关的硬件知识还要掌握相关的操作系统的实现,这样才能更好的结合C++的软件开发,展开CPU、IO等相关缓存处理的优化。

相关推荐
小张成长计划..2 小时前
【C++】37:IO库(扩展)
c++
Cx330❀2 小时前
【Qt 核心机制篇】深度解析 Qt 信号与槽(Signals & Slots)机制:从底层原理、实战演练到 Lambda 进阶
linux·开发语言·c++·人工智能·qt·ubuntu
学习,学习,在学习2 小时前
Modbus TCP同步通信方式实现异步级效率
网络·c++·qt·网络协议·tcp/ip·qt5
Cx330❀2 小时前
【Linux网络】从零构建高性能UDP服务器:从Echo到英译汉业务级实现
大数据·linux·服务器·开发语言·网络·c++·udp
闪电悠米2 小时前
黑马点评-优惠券秒杀-03_basic_seckill_and_oversell
java·数据库·spring boot·spring·缓存·oracle·面试
不吃土豆的马铃薯2 小时前
TCP 三次握手 / 四次挥手详解
服务器·开发语言·网络·c++·网络协议·tcp/ip
羑悻的小杀马特2 小时前
【动态规划篇】正则表达式与通配符:开启代码匹配的赛博奇幻之旅
c++·算法·leetcode·正则表达式
Huangjin007_2 小时前
【C++ STL篇(十三)】无序关联容器 unordered_set / unordered_map解析
开发语言·c++
Mortalbreeze2 小时前
C++11 ---- 列表初始化
c++