第4章 ARMv9缓存架构:从L1到硬件一致性
缓存这东西,说白了就是CPU和内存之间的“加速器”。我刚开始做ARMv8的时候,总觉得缓存配置照着参考设计抄就行。直到有一次,一个客户的产品在跑视频编解码时频繁卡顿,我花了整整两周才定位到是L2缓存的替换策略没配对。从那以后,我对缓存架构再也不敢掉以轻心了。
ARMv9的缓存体系,比以往任何一代都复杂。它不仅要管数据快不快,还要管多个核之间数据一不一致。咱们这一章,就把L1/L2/L3、MESI/MOESI协议、缓存维护指令,还有硬件一致性这几个硬骨头,一个一个啃下来。
4.1 缓存层级:L1、L2、L3的职责分工
ARMv9的缓存层级,你可以想象成一个“三级响应体系”。L1离核心最近,速度最快,但容量最小。L2作为中坚力量,容量和延迟都居中。L3则是多个核心共享的“大仓库”。
我个人习惯把这三级的参数记成一张表,调试时翻出来对照特别方便:
| 层级 | 典型容量 | 典型延迟 | 关联方式 | 归属 |
|---|---|---|---|---|
| L1 I-Cache | 32KB - 64KB | 1-2 cycles | 2/4路组相联 | 每核私有 |
| L1 D-Cache | 32KB - 64KB | 1-2 cycles | 2/4路组相联 | 每核私有 |
| L2 Cache | 128KB - 512KB | 8-12 cycles | 8/16路组相联 | 每核私有或簇共享 |
| L3 Cache | 1MB - 8MB | 20-40 cycles | 16/32路组相联 | 所有核共享 |
这里有个关键点:L1是分指令缓存和数据缓存的,L2和L3则是统一的。为什么这么设计?因为指令流和数据流的访问模式完全不同。指令是顺序执行的,数据是随机跳转的。混在一起,反而容易互相干扰。
4.2 缓存一致性协议:MESI与MOESI
多核系统最头疼的问题是什么?数据打架。核A改了变量x,核B不知道,还在用旧值。这就乱套了。缓存一致性协议,就是用来解决这个问题的。
ARMv9支持两种协议:MESI和MOESI。它们都基于“监听”或“目录”机制,但状态机略有不同。
4.2.1 MESI协议的四态
MESI这个名字,就是四个状态的缩写:
- M (Modified):数据被修改,且只在本缓存中。内存中的副本已过时。
- E (Exclusive):数据未被修改,且只在本缓存中。内存中的副本是最新的。
- S (Shared):数据未被修改,且可能存在于多个缓存中。内存中的副本是最新的。
- I (Invalid):数据无效,不能使用。
状态转换的规则,我画了一张图,你一看就明白:
4.2.2 MOESI协议的增强
MOESI在MESI基础上多了一个O(Owned)状态。这个O状态有什么用?我举个例子你就懂了。
假设核A把数据改成了M状态,核B想读这个数据。在MESI里,核A必须先把数据写回内存,然后两个核都变成S状态。但在MOESI里,核A可以直接把数据“分享”给核B,自己变成O状态,核B变成S状态。内存不需要更新。
这样做的好处是:减少了写回内存的次数。在ARMv9的DSU-110中,MOESI是默认启用的。我个人建议,除非你有非常特殊的理由,否则不要关闭它。
4.3 缓存维护指令:DC CVAC/ISW等
协议是硬件自动维护的,但有些场景下,软件必须主动干预。比如DMA传输、多核通信、电源管理。这时候,就需要缓存维护指令出场了。
ARMv9提供了一套完整的缓存维护指令,我挑几个最常用的给你讲:
| 指令 | 全称 | 作用 | 典型场景 |
|---|---|---|---|
| DC CVAC | Clean Virtual Address to Point of Coherency | 将指定虚拟地址的缓存行写回,直到一致性点 | DMA发送前,确保数据已写回内存 |
| DC CIVAC | Clean and Invalidate Virtual Address to Point of Coherency | 写回并失效指定缓存行 | DMA接收后,使缓存失效以读取新数据 |
| DC ISW | Invalidate Set/Way | 按Set/Way失效整个缓存 | 电源管理、热插拔 |
| IC IVAU | Instruction Cache Invalidate by Virtual Address to Point of Unification | 失效指令缓存中的指定行 | 自修改代码(SMC) |
写代码的时候,我习惯这样用:
// DMA发送前:确保数据从缓存写回内存
__asm volatile("DC CVAC, %0" : : "r" (buf_addr) : "memory");
// DMA接收后:使缓存失效,从内存重新读取
__asm volatile("DC CIVAC, %0" : : "r" (buf_addr) : "memory");
// 自修改代码:先失效指令缓存,再执行屏障
__asm volatile("IC IVAU, %0" : : "r" (code_addr) : "memory");
__asm volatile("DSB ISH" : : : "memory");
__asm volatile("ISB" : : : "memory");
4.4 硬件一致性:ACE与CHI
前面讲的MESI/MOESI,都是针对CPU核之间的缓存一致性。但现代SoC里,除了CPU,还有GPU、NPU、DSP、DMA控制器。这些设备怎么和CPU保持一致?
ARMv9的答案是:硬件一致性接口。目前主流的有两个:ACE和CHI。
4.4.1 ACE接口
ACE是ARMv8时代的标准。它通过监听总线实现一致性。每个ACE设备都能“看到”其他设备对缓存行的操作。优点是实现简单,缺点是总线带宽压力大。
4.4.2 CHI接口
CHI是ARMv9主推的新一代一致性接口。它采用“目录式”管理,不再依赖广播监听。每个缓存行都有一个“归属节点”(Home Node),所有一致性请求都通过这个节点协调。
我画个对比图,你感受一下差异:
CHI的优势在大规模系统中特别明显。我记得有一次调试一个16核的服务器芯片,ACE的监听总线带宽成了瓶颈,换成CHI后,一致性延迟降低了40%。
4.5 实战:缓存一致性问题的调试方法
理论讲完了,咱们来点实际的。怎么判断缓存一致性问题?我一般用三步法:
- 看性能计数器:ARMv9的PMU提供了缓存一致性相关的事件,比如
BUS_ACCESS、SNOOP_ACCESS、SNOOP_RETRY。如果SNOOP_RETRY占比高,说明一致性协议在频繁重试。 - 加调试打印:在关键数据结构的访问前后,打印时间戳和值。如果两个核打印的值不一致,那就是一致性问题。
- 用硬件断点:在数据地址上设置硬件断点,观察是哪个核在什么时候修改了它。ARM DS-5和DS-1000都支持这个功能。
我曾经用这个方法,帮一个客户定位到了GPU和CPU之间的数据竞争问题。问题出在GPU驱动里漏了缓存维护指令,加上一条DC CIVAC就解决了。