3、直接映射Cache:地址划分(Tag、Index、Offset),硬件实现与冲突缺失
好,咱们今天聊聊直接映射Cache。这是Cache设计里最基础、最直观的一种方式。说白了,就是每个内存地址,在Cache里只有一个固定的“家”。你想想看,这设计起来多省事?
3.1 地址划分:Tag、Index、Offset
要理解直接映射,先得搞明白CPU发出的地址是怎么被拆解的。一个内存地址,在Cache眼里,被分成了三部分:Tag(标签)、Index(索引)、Offset(块内偏移)。
我个人习惯把Cache想象成一个旅馆。旅馆有很多房间(Cache行),每个房间能住几个人(字节)。那地址里的Index,就是房间号。CPU拿着地址,先看Index,就知道该去哪个房间找人。
Offset呢?就是进了房间后,具体找哪个人。比如一个Cache行有64字节,那Offset就是用来定位这64字节里的第几个字节。
最后是Tag。这玩意儿最容易被忽略。你想想,不同内存地址可能映射到同一个房间号(Index)。那怎么区分?就得靠Tag。Tag就像是住客的身份证号,用来确认这个房间里的人,是不是你要找的那个。
举个例子,假设Cache大小64KB,每行64字节,那总共有1024行(64KB / 64B)。Index就需要10位(2^10=1024)。Offset需要6位(2^6=64)。剩下的高位全是Tag。
核心公式:
- Offset位数 = log2(块大小)
- Index位数 = log2(Cache行数)
- Tag位数 = 地址总位数 - Index位数 - Offset位数
3.2 硬件实现:其实没那么复杂
直接映射的硬件实现,可以说是最简单的。我刚开始做设计时,觉得Cache肯定很玄乎,结果一看直接映射的电路,嗯,就这?
核心硬件就三块:地址译码器、Tag比较器、数据存储器。
- 地址译码器:拿Index去查,找到对应的Cache行。
- Tag比较器:把地址里的Tag,和Cache行里存的Tag做比较。相等就是命中,不相等就是缺失。
- 数据存储器:如果命中,就用Offset从数据块里取出对应的字节。
你看,逻辑链非常短。没有复杂的替换算法,没有多路选择。这也是为什么直接映射能做到单周期访问的原因。
避坑指南: 我曾经在一个低功耗项目里,为了省面积选了直接映射。结果发现Tag比较器的位宽不能省。Tag位数越多,比较器面积越大。别为了省几个触发器,把比较器搞成瓶颈。
下面这张图,是我手绘的直接映射Cache结构。你一看就明白。
3.3 冲突缺失:直接映射的阿喀琉斯之踵
直接映射最大的问题,就是冲突缺失。为什么?因为每个地址只能去一个固定的位置。如果两个经常访问的地址,恰好Index相同,那它们就会互相“打架”。
举个例子,假设地址A和地址B的Index都是5。程序先访问A,A被加载到行5。接着访问B,B发现行5被占了,而且Tag不匹配,于是把A踢出去,自己住进来。等会儿再访问A,A又发现行5被B占了,又把B踢出去。这就叫颠簸(Thrashing)。
注意: 冲突缺失和容量缺失不一样。容量缺失是Cache太小,装不下工作集。冲突缺失是Cache够大,但映射策略太死板,导致数据互相挤占。我见过一个图像处理算法,因为两个核心数组的步长刚好是Cache大小的整数倍,导致性能直接腰斩。查了半天才发现是冲突缺失。
怎么解决?最简单的办法是提高关联度,比如改成组相联。但直接映射也有它的好处:硬件简单、功耗低、访问速度快。在一些对面积和功耗极度敏感的场景(比如嵌入式MCU),直接映射依然是首选。
| 特性 | 直接映射 | 全相联 | 组相联 |
|---|---|---|---|
| 硬件复杂度 | 低 | 高 | 中 |
| 访问速度 | 快(单周期) | 慢(需遍历) | 中 |
| 冲突缺失率 | 高 | 低 | 中 |
| 功耗 | 低 | 高 | 中 |
嗯,直接映射Cache就讲这么多。记住它的核心:一个地址,一个位置,简单粗暴。但代价就是冲突缺失。设计时一定要根据应用场景来权衡。
总结一下:
- 地址划分:Tag(身份)、Index(房间号)、Offset(房间内位置)
- 硬件实现:译码器 + 比较器 + 存储器,逻辑简单
- 冲突缺失:多个地址争抢同一个Cache行,导致频繁替换