3. 缓存优化技术:缓存分区与路预测、缓存压缩、写缓冲与写合并、非阻塞缓存与预取

缓存,说白了就是芯片的「临时记忆」。但这份记忆的功耗,往往能占到整个芯片的 30%-50%。我见过太多团队,CPU 算力做得飞快,结果一测功耗,缓存漏电和动态切换占了半壁江山。嗯,今天我们就来聊聊怎么把这部分功耗降下来。

3.1 缓存分区与路预测

先问一个问题:为什么缓存要分区?

传统缓存是全相联或组相联的。每次访问,所有路(Way)都要同时比较 Tag。这就像你进图书馆,管理员把所有书架都翻一遍——功耗自然高。我个人习惯的做法是:把缓存分成多个独立的子区,每次只激活可能命中那个区。

3.1.1 缓存分区(Cache Partitioning)

缓存分区有两种主流方式:

  • 静态分区:在编译或启动时固定分配。比如 L1 数据缓存分成 4 个 8KB 的 bank,每个核心或线程独占一个 bank。优点是实现简单,缺点是利用率不高。
  • 动态分区:运行时根据访问模式调整。我记得在某个 AI 加速器项目中,我们用了基于「重用距离」的动态分区算法,把缓存命中率提升了 12%,同时动态功耗下降了 18%。
我的经验: 分区粒度不要太小。我曾经试过 1KB 一个分区,结果控制逻辑的功耗比省下来的还多。一般建议分区大小在 4KB-16KB 之间。

3.1.2 路预测(Way Prediction)

路预测是另一种思路。它不分区,而是预测「这次访问最可能命中哪一路」。预测对了,只激活那一路的 Tag 比较;预测错了,再全路查找。

我常用的路预测策略有三种:

策略 原理 功耗节省 命中率影响
MRU(最近最多使用) 预测命中最近使用的那一路 30%-40% 几乎无影响
PC-based 预测 根据程序计数器(PC)历史记录预测 40%-50% 命中率下降 1%-3%
混合预测 MRU + PC 结合,带置信度计数器 45%-55% 命中率下降 <1%
避坑指南: 我曾经在一个视频编解码芯片上用了纯 PC-based 预测,结果遇到多线程切换时,预测准确率直接掉到 60%。后来加了线程 ID 作为预测因子才解决。记住:路预测的准确率高度依赖访问模式的稳定性。

3.2 缓存压缩技术

缓存压缩,说白了就是用更少的存储空间装更多的数据。你想想看,如果缓存能压缩到原来的一半大小,那漏电功耗直接砍半,动态功耗也因为访问次数减少而下降。

3.2.1 常见压缩算法

在缓存里做压缩,不能太复杂。解压延迟超过 2 个周期,基本就没意义了。我常用的有:

  • BDPI(Base-Delta-Immediate):适合数值相近的数据,比如数组元素。压缩率 1.5x-2x,解压延迟 1 周期。
  • FPC(Frequent Pattern Compression):识别常见数据模式(全 0、全 1、符号扩展等)。压缩率 1.3x-1.8x,解压延迟 1-2 周期。
  • 字典压缩:维护一个小型字典,重复数据只存索引。适合有大量重复值的场景,比如稀疏矩阵。

3.2.2 压缩缓存架构

我参与过一个 RISC-V 处理器的 L2 缓存压缩项目,架构是这样的:

压缩缓存架构图 CPU 核心 压缩/解压引擎 压缩缓存阵列 元数据表 工作流程: 1. CPU 发出访存请求 → 2. 压缩引擎检查元数据表,判断数据是否已压缩 3. 若已压缩,解压后返回;若未压缩,从缓存阵列读取原始数据 4. 写回时,压缩引擎尝试压缩,若压缩成功则存储压缩格式并更新元数据 未压缩缓存 动态功耗:1.0x(基准) 漏电功耗:1.0x(基准) 压缩缓存(BDPI) 动态功耗:0.65x(-35%) 漏电功耗:0.55x(-45%)
关键数据: 在那个项目中,我们用了 BDPI 压缩,L2 缓存的有效容量从 256KB 提升到约 450KB(等效),动态功耗下降 35%,漏电功耗下降 45%。代价是增加了 2 周期的读延迟和 3 周期的写延迟。

3.3 写缓冲与写合并

写操作是缓存功耗的大头。为什么?因为写操作通常需要先读(Read-Modify-Write),而且写回主存的功耗极高。写缓冲和写合并就是用来「攒一批再写」的技术。

3.3.1 写缓冲(Write Buffer)

写缓冲是一个 FIFO 队列,CPU 写缓存时,数据先进入写缓冲,然后异步写入缓存阵列。好处是:

  • CPU 不用等写完成,减少流水线停顿
  • 多个写操作可以合并,减少对缓存阵列的访问次数

我一般建议写缓冲深度为 4-8 项。太浅了合并效果差,太深了控制逻辑复杂且面积大。

3.3.2 写合并(Write Merging)

写合并是写缓冲的进阶版。当多个写操作命中同一个缓存行(Cache Line)时,把它们合并成一个写操作。举个例子:

// 原始写操作序列(假设缓存行大小为 64 字节)
写地址 0x1000,数据 0x12345678(4 字节)
写地址 0x1004,数据 0x9ABCDEF0(4 字节)
写地址 0x1008,数据 0x11223344(4 字节)

// 写合并后
写地址 0x1000,数据 0x112233449ABCDEF012345678(12 字节,合并成一次写)
我的习惯: 写合并的粒度最好对齐到缓存行。我曾经试过按 4 字节粒度合并,结果控制逻辑复杂到差点把时序搞崩。对齐到缓存行后,合并逻辑简单了,功耗也降了 10%。

3.4 非阻塞缓存与预取

最后聊聊非阻塞缓存和预取。这两个技术看似是性能优化,但用好了,功耗也能降不少。

3.4.1 非阻塞缓存(Non-blocking Cache)

传统缓存遇到 Miss 时,会阻塞 CPU 直到数据回来。非阻塞缓存允许在 Miss 处理期间继续处理后续的 Hit 请求。这减少了 CPU 的空转时间,间接降低了功耗。

非阻塞缓存的核心是 Miss Status Holding Register(MSHR)。每个 MSHR 记录一个未完成的 Miss 请求。我常用的配置是:

参数 推荐值 说明
MSHR 数量 4-8 太少无法隐藏延迟,太多增加功耗
每个 MSHR 的合并能力 2-4 次 多个 Miss 到同一缓存行可合并
MSHR 查找方式 全相联 面积大但查找快,适合少量 MSHR

3.4.2 预取(Prefetching)

预取是在 CPU 真正需要数据之前,提前把数据加载到缓存里。预取做得好,缓存 Miss 率下降,主存访问次数减少,功耗自然降低。

我常用的预取策略:

  • 顺序预取:访问地址 A 后,预取 A+1、A+2... 适合数组遍历。预取深度一般 2-4 条缓存行。
  • 步长预取:检测固定步长的访问模式(如 A, A+16, A+32...)。适合矩阵运算。
  • 基于 PC 的预取:记录每个指令的历史访问模式,预测下一次访问地址。
我曾经踩过的坑: 在一个图像处理芯片上,我开了深度为 8 的顺序预取。结果图像数据是隔行扫描的,预取进来的数据 80% 都没用,反而把有用的数据挤出去了。后来改成自适应预取深度,根据 Miss 率动态调整,才把功耗降下来。记住:预取是把双刃剑,预取错了比不预取更耗电。

3.5 本章小结

缓存优化不是单一技术能搞定的。我个人的经验是:先做分区或路预测,把动态功耗降下来;再考虑压缩,降低漏电功耗;写缓冲和写合并是「顺手」就能做的优化;非阻塞缓存和预取则需要在性能和功耗之间找平衡。你想想看,这些技术组合起来,缓存功耗降低 40%-60% 是完全可行的。