3. Cache基本原理:直接映射、组相联、全相联,命中率与替换策略

各位同学,咱们今天聊聊Cache。说实话,我在基站项目里跟Cache打交道最多的时候,就是调优实时性任务的时候。你想想看,一个数据包从天线进来,经过层层协议栈,如果Cache没用好,延迟直接翻倍。这可不是闹着玩的。

Cache说白了,就是CPU和主存之间的一块“小抄本”。CPU要数据,先翻小抄本,翻到了叫“命中”,翻不到叫“缺失”,得去主存里找。这小抄本怎么设计,直接决定了命中率。今天咱们就把三种映射方式、命中率计算、以及替换策略讲透。

3.1 直接映射:简单粗暴,但容易打架

直接映射,是我最早接触的Cache结构。它的规则很简单:主存中的每个数据块,只能放到Cache中唯一的一个位置。

怎么算这个位置?用地址对Cache行数取模。比如Cache有64行,主存地址0x1000,那就放到第(0x1000 % 64)行。

核心公式:Cache行号 = (主存块地址) mod (Cache总行数)

我在一个早期的基站控制面项目中,就吃过直接映射的亏。当时有个循环访问数组的程序,数组大小刚好是Cache大小的整数倍。结果呢?每次访问都映射到同一行,反复踢出、加载,命中率几乎为零。这就是所谓的“Cache颠簸”。

避坑指南:我曾经因为数组步长设置不当,导致直接映射Cache颠簸,性能直接腰斩。后来我养成了一个习惯:凡是循环访问大数组,先算一下步长和Cache大小的关系。

优点:硬件简单,查表快,适合对延迟敏感的场景。

缺点:冲突率高,容易颠簸。

3.2 全相联:灵活但慢,适合小容量

全相联是另一个极端。主存中的任何数据块,可以放到Cache中的任意位置。你想想看,这多灵活!

但问题来了:CPU要找一个数据,得把Cache里所有行都查一遍。这硬件开销太大了。我做过一个实验,全相联Cache的查找延迟,比直接映射高了3-5倍。所以实际中,全相联只用在TLB(页表缓存)这种小容量场景。

个人经验:我在设计基站的内存管理单元时,TLB用了全相联。因为TLB条目少(通常几十条),全相联的命中率优势能充分发挥。但L1/L2 Cache绝对不能用,太慢了。

优点:冲突率最低,空间利用率最高。

缺点:查找慢,硬件复杂,不适合大容量。

3.3 组相联:折中方案,工程首选

组相联是直接映射和全相联的“混血儿”。它把Cache分成若干组,每组内有若干行。数据先通过地址映射到某个组,然后在组内可以放在任意一行。

举个例子:4路组相联,就是每组有4行。数据先算组号(类似直接映射),然后在组内4行中自由选择(类似全相联)。

相联度 组内行数 冲突率 查找延迟 典型应用
直接映射 1 早期嵌入式CPU
2路组相联 2 低端MCU
4路组相联 4 中高 基站DSP、ARM Cortex-A
8路组相联 8 极低 高性能服务器CPU
全相联 全部 无冲突 最高 TLB、小容量缓存

我个人习惯,在基站基带处理芯片中,L1 Cache用4路组相联。为什么?因为基带算法有大量规则的数据访问模式,4路足够消除大部分冲突,同时延迟可控。

3.4 命中率:不是越高越好

很多人以为命中率越高越好。其实不然。我见过一个团队,为了把命中率从95%提到98%,把Cache容量翻了一倍。结果芯片面积大了30%,功耗涨了20%,但性能只提升了3%。

命中率受几个因素影响:

  • 容量:Cache越大,命中率越高,但成本和功耗也高。
  • 相联度:相联度越高,冲突越少,命中率越高。
  • 块大小:块太大,浪费空间;块太小,空间局部性利用不足。
  • 程序行为:时间局部性和空间局部性好的程序,命中率天然高。

经验之谈:在基站系统中,我通常把L1 Cache命中率目标定在90%-95%。再往上,投入产出比就划不来了。省下来的钱,不如优化算法。

3.5 替换策略:谁该滚蛋?

当Cache满了,新数据要进来,就得踢走一个“老住户”。怎么选?这就是替换策略的事。

LRU(最近最少使用):踢走最长时间没被访问的那个。这是最常用的策略。我做过测试,LRU在大多数场景下,命中率比随机替换高10%-15%。

FIFO(先进先出):谁先来谁先走。简单,但效果一般。因为最早进来的数据,可能正是频繁使用的。

随机替换:随机选一个踢走。硬件最简单,但命中率不稳定。我见过一个极端案例:随机替换导致关键数据被踢,系统实时任务超时。

LFU(最不经常使用):踢走访问次数最少的。听起来合理,但实现复杂,而且对突发访问不友好。

我的建议:在基站嵌入式系统中,LRU是首选。如果硬件资源紧张,可以用“伪LRU”(用少量位近似模拟LRU)。我曾经在FPGA上实现过伪LRU,只用了3个额外bit,效果接近真LRU。

3.6 实战:如何选择Cache参数

好了,理论讲完了。咱们来点实际的。假设你要设计一个基站L1数据Cache,怎么选参数?

  1. 先看工作负载:基带算法是流式处理,还是随机访问?流式处理适合大块、直接映射;随机访问适合小块、高相联度。
  2. 再算面积预算:Cache占芯片面积很大。我一般用“每KB面积”来估算。4路组相联比直接映射大约多30%面积。
  3. 最后做仿真:用真实程序跑一下,看命中率和延迟。别光看理论值。

重要提醒:我曾经犯过一个错:只看了平均命中率,没看最坏情况。结果在某个极端场景下,Cache命中率骤降到40%,系统直接崩了。记住,嵌入式系统要关注“最坏情况执行时间”,不是平均值。

好了,这一章就到这里。Cache这东西,说难不难,说简单也不简单。关键是多动手、多踩坑。下一章咱们讲Cache一致性,那才是真正让人头疼的地方。