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比较器、数据存储器

  1. 地址译码器:拿Index去查,找到对应的Cache行。
  2. Tag比较器:把地址里的Tag,和Cache行里存的Tag做比较。相等就是命中,不相等就是缺失。
  3. 数据存储器:如果命中,就用Offset从数据块里取出对应的字节。

你看,逻辑链非常短。没有复杂的替换算法,没有多路选择。这也是为什么直接映射能做到单周期访问的原因。

避坑指南: 我曾经在一个低功耗项目里,为了省面积选了直接映射。结果发现Tag比较器的位宽不能省。Tag位数越多,比较器面积越大。别为了省几个触发器,把比较器搞成瓶颈。

下面这张图,是我手绘的直接映射Cache结构。你一看就明白。

CPU地址: Tag (20位) Index (10位) Offset (6位) Index译码 Cache 数据阵列 (1024行) 行0: Tag=0x12345 数据块 (64字节) 有效位=1 行1: Tag=0x6789A 数据块 (64字节) 有效位=0 ...... 行1023: Tag=0xFFFFF 数据块 (64字节) 有效位=1 Tag比较 命中/缺失 数据输出

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行,导致频繁替换

专注资料整理