urain39 / stuff

Noting here.
1 stars 0 forks source link

CPU 中的 Cache 实现 #128

Open urain39 opened 1 year ago

urain39 commented 1 year ago

基础知识点

Cache 的组成

CPU 中的 Cache 一般由缓存行(Cache Line)、缓存集(Cache Set)组成。缓存行是由 2^B 个连续字节组成的缓存的基本单位;而缓存集则是一组缓存行的容器。

B 的取值范围一般是 5~8。

Cache 查找过程

CPU 中的 Cache 查找是通过拆分地址实现的。一般情况下,地址会拆分为 TAG、INDEX、OFFSET 三个部分。

首先 Cache 会使用 INDEX 索引地址所在的缓存集;然后使用 TAG 找到准确的地址;最后使用 OFFSET 获取到实际值。

为什么要有 INDEX?

INDEX 的作用是为了优化查询。正是因为它的存在,我们才能将 C 个缓存行的查找范围缩小到 C / S 次(S 是缓存集数量)。

INDEX 能放到 TAG 前面吗?

理论可行,但实际不行。使用高位作为 INDEX 会导致我们在访问固定地址段的内存时,不能最大化利用缓存容量(都映射到同一缓存集里了)。

我居然真的在网上看到有些文章把 TAG 和 INDEX 搞反了!

N 路缓存集关联

如果缓存集的容量为 N,那么我就叫其 N 路(N-way)。如缓存集中有8个缓存行,那么我们就将其叫做8路。

N-way 全称是 N-way set associative(N 路缓存集关联)。

当 N 为1时,我们将其称为直接映射(Direct Mapped)。这种情况下的查找效率最高,但灵活性较差(无法引入替换策略)。

还有个全关联(fully associative)的概念。它没有 INDEX,查找效率非常低。你可以看作是只有一个缓存集的情况。这里就不细讲了。

进阶知识点

引入替换策略

仅仅了解 CPU 中的 Cache 组成和查找过程是远远不够的。我们还需要学习一个非常重要的概念——替换策略(Replacement Policy)。

缓存的大小是固定的,它肯定也会有写满的时候。如果此时有新数据写入,那么我们就不得不从已使用的缓存中选择一块区域使用。

这个过程完全取决于替换策略。常见的实现按命中率从低到高主要有 FIFO、Random、Pseudo-LRU 三种。

替换策略只针对缓存集。这是因为引入 INDEX 对查找优化后,缓存行都被拆散在缓存集里了。当然,没有 INDEX 另说。

选择合适的替换策略

使用哪种替换策略取决于预算成本。对于高性能 CPU,我推荐使用 Pseudo-LRU;对于入门级 CPU 我推荐 Random;实在不行再使用 FIFO。

我听说 MIPS 好像很喜欢用 Random 替换策略……另外真的有人使用 FIFO 替换策略吗?

如何设计缓存

给 6502 添加缓存!

6502 是70年代中期到80年代末期最成功的 CPU 之一。它的字长为 8-bit,拥有 16-bit 长的地址总线,可寻址 64 KiB 空间。

在添加缓存前,我们需要先确定缓存大小。一般而言,通过地址总线折半算法可以直接得到这个值。即 16-bit 折半为 8-bit,得到缓存大小为 256 字节。

64-bit 之后,这个方法就不好用了。因为我们的内存并没有真的利用上这么长的地址总线。最多也只是利用到了 40-bit 左右。

缓存大小确定后,我们也就能依此确定 INDEX、OFFSET的长度。我们依据上面的 Cache 组成,可以得到缓存大小的公式为N * 2^(INDEX + OFFSET)。N 为 N 路缓存。

我们将实际值代入,可以得到方程N * 2^(INDEX + OFFSET) = 256。这个方程并没有固定解,我们按需求分配就行。我使用的解是 N=4, INDEX=2, OFFSET=4。剩下的部分全分给 TAG。

吹毛求疵(选读)

考虑到 8-bit CPU 的特殊性,我们不建议 TAG、INDEX、OFFSET 任意一个长度大于8。上面的设计换算后,TAG=10,就已经超出这个值了。

要解决这个问题,我们就必须扩大缓存大小或者缩小缓存集容量。如扩大缓存,那么可以将解改成 N=2, INDEX=2, OFFSET=6(512字节缓存);或者缩小缓存集容量,将解改成 N=1, INDEX=2, OFFSET=6(256字节直接映射缓存)。

参考/引用: