第一章:缓存基础与原理

各位同学好,我是你们的老朋友。今天咱们聊聊嵌入式推理里最基础、也最容易被忽视的东西——CPU缓存。

说实话,我刚开始做嵌入式AI优化那会儿,根本没把缓存当回事。总觉得算法调好了,模型量化了,性能自然就上去了。结果呢?跑起来卡得跟幻灯片似的。后来一查,发现大部分时间都花在了等数据上。嗯,从那以后,我再也不敢小看缓存了。

1.1 CPU缓存层次结构(L1/L2/L3)

先说说缓存的层次。你想想看,CPU跑得飞快,内存却慢得像蜗牛。这中间得有个缓冲地带。于是就有了L1、L2、L3三级缓存。

  • L1缓存:离CPU核心最近,速度最快,但容量最小。一般就32KB到64KB。我习惯把它比作「桌面上的工具」,随手就能拿到。
  • L2缓存:稍微远一点,容量大一些,256KB到1MB左右。相当于「抽屉里的备件」,拿起来也快,但比桌面慢一丢丢。
  • L3缓存:多个核心共享的,容量最大,几MB到几十MB。但速度也最慢。你可以理解为「仓库里的存货」,得跑一趟才能取到。

我在项目中遇到过一个问题:模型推理时,L1缓存命中率只有60%多。当时百思不得其解,后来发现是数据布局太分散,导致频繁的缓存缺失。说白了,就是CPU老得去「仓库」搬数据,能不慢吗?

核心要点:推理优化,第一步就是让数据尽量待在L1缓存里。别让它老往L2、L3甚至内存跑。

1.2 缓存行与缓存组

缓存不是按字节存的,而是按「行」存的。一行通常64字节。为什么是64?因为局部性原理——你访问了一个地址,附近的数据很可能也会被用到。

举个例子。你读了一个int(4字节),CPU会把连着它后面的60字节也一起拉进缓存。这叫「空间局部性」。我刚开始做推理优化时,经常忽略这个特性。后来发现,把相邻的权重数据放在一起,命中率能提升一大截。

缓存组又是什么?说白了,就是缓存行的集合。每个组里可以放好几行。CPU通过地址的某些位来决定数据该放到哪个组里。这有点像图书馆的书架——每本书都有固定的分类号,你得按号找位置。

个人经验:设计数据结构时,尽量让连续访问的数据落在不同的缓存组里。否则会出现「组冲突」,导致频繁替换。我曾经因为一个数组的步长设置不当,让所有数据都挤在同一个组里,性能直接腰斩。

1.3 缓存替换策略(LRU、LFU、随机)

缓存满了怎么办?得把旧数据踢出去,换新的进来。这就涉及替换策略了。

策略 原理 优缺点
LRU(最近最少使用) 踢掉最久没被访问的那一行 效果好,但硬件开销大
LFU(最不经常使用) 踢掉访问次数最少的那一行 适合长期热点数据,但实现复杂
随机替换 随机选一行踢掉 简单粗暴,但性能不稳定

我个人习惯用LRU。虽然硬件实现复杂点,但效果最稳定。不过要注意,LRU在循环访问大数组时会有问题——所有数据都被频繁访问,LRU反而会频繁替换,导致「缓存抖动」。

避坑指南:我曾经在推理循环里用了一个大数组,每次迭代都访问全部数据。结果LRU策略下,缓存行被反复踢进踢出,命中率不到30%。后来改成「分块处理」,把数据切成小块,每块都能塞进缓存,命中率直接飙到90%以上。

1.4 缓存一致性协议(MESI)

多核处理器里,每个核心都有自己的L1缓存。如果两个核心同时修改同一份数据,怎么办?这就得靠MESI协议了。

MESI是四个状态的缩写:

  • M(Modified):数据被修改了,和内存不一致。只有当前核心有这份数据。
  • E(Exclusive):数据没被修改,但只有当前核心有。和内存一致。
  • S(Shared):数据没被修改,多个核心都有。和内存一致。
  • I(Invalid):数据无效,得重新从内存加载。

你想想看,如果核心A修改了数据,核心B还在用旧数据,那推理结果就全错了。MESI协议就是用来保证这种「一致性」的。

我在做多核推理加速时,就踩过这个坑。两个核心同时处理不同的输入数据,但共享了部分权重。结果因为缓存一致性的开销,性能反而比单核还差。后来我把权重数据做了「只读」标记,让所有核心共享同一份只读缓存,才解决了问题。

关键点:多核推理时,尽量让数据是「只读」的。写操作会触发缓存一致性协议,带来额外的延迟。能避免就避免。

小结

好了,这一章咱们把缓存的基础过了一遍。从层次结构到缓存行,从替换策略到一致性协议。说实话,这些知识看起来枯燥,但实际优化时,每一个细节都可能成为性能瓶颈。

下一章,我会结合具体的推理模型,讲讲怎么利用这些缓存原理来优化代码。到时候咱们再细聊。