Skip to content

Basics

如上图,对于多周期 CPU,往往取 mem 的时候用多周期去取,而且会有一个 ready bit。

Cache

如上图,可以参考我在 csapp 中的笔记。

Example: the intrinsity FastMATH Processor

如上图,这个 cache 的参数是:

  • block size: 16 words = 64 bytes = \(2^6\)​ bytes
  • corresponding memory: 16 KB = \(2^{14}\)​ bytes

从而,我们的

  • block offset bits: 6
  • index bits: 14 - 6 = 8
  • tag bits: 32 - 10 - 6 = 16

具体的实现中,可以看到:

  1. mux 的输出式 32 位的,且忽略地址的前两位(也就是默认地址必须是 4 的倍数,也就是取的值是 4 bytes = 1 word 的)
    • 说明这个 cache 是 word-addressable 的
  2. Hit bit 跟两个有关:cache.valid_bit ?= 1,以及 cache.tag ?= input.tag。

Test: SPEC2000

使用 SPEC2000 benchmark 进行测试,结果如下:

Instruction miss rate Data miss rate Effective combined miss rate
0.4% 11.4% 3.2%

由于 instruction 一般而言是顺序执行的,因此 locality 一般比较好。

而 data 一般而言总是难以避免随机读取,而且可能就只读一个值,因此 data miss rate 一般而言比较差。

Split Cache and Combined Cache

我们的流水线用的是 split cache,为的就是避免结构竞争。

Cache Performance Analysis

如上图,如果要将 4 个 block 从内存传到 cache,那么就需要:

  1. 1 cycle 将地址传给 memory
  2. 15 cycles 等待取内存数据
  3. 1 (bus) cycle 数据从总线上传回 CPU

由于数据从总线上传回 CPU 这一步的同时可以传第下一个 address,因此总共耗时:\((1 + 15) * 3 + (1 + 15 + 1) = 65\)

由于内存数据需要先写到 cache 里,然后再花费一个周期从 cache 读入寄存器,因此,一共需要 66 个 clock cycles。

Observation

从而,在 miss rate = 10% 的情况下,使用 cache 和不用 cache 的性能是有显著差距的。

但是,我们也明显看到:使用 cache 需要 7.5 clks,其中 6.6 的 clk cycle 就是白白等待 cache。

这说明,我们的 cache 有待改进:reduce miss rate


同时,我们也可以看到,如果我们将 block size 上调至 16 words(同时假设 miss rate 不变),使用 cache 甚至比不用 cache 更加耗时。这一切的原因,就是无法并行读取 memory。

这说明,我们的 memory 也需要跟 cache 配套:reduce miss penalty

Reduce Miss Penalty

如图,传统的 memory 就是第一种——每次只能传一个 byte。

一个显然的想法是,我们可以增加带宽,从而一次可以传多个 bytes。

  • 这种方式是不常用的,一个原因是如果需要加宽总线的话,代价太高

因此,我们就想:传输并不是瓶颈,获取地址才是。因此,与其总线加宽(并行传输+并行读取),不如就仅仅并行读取。具体方法如下:

我们将一个 word 的 4 个不同的 bytes 放在 4 个不同的 memory banks 里,然后 memory bank 取数据是并行/分时启动的,但是传输数据是串行的。

  • 由于取数据是瓶颈,因此这种方式也很好
  • 这种方式是现在的主流

Wide Memory

如图:如果传输的线宽为 2 words (8 bytes),那么我们就可以只需要传输两次,而不是 16 次。

图示如下:

Interleaved Memory

如图:如果线宽就是 1 个 word,但是分为 4 个 banks,然后 bank 的之间的启动时间间隔为 1 cycle(从而取出数据的时间也是 1 cycle)

图示如下:

如果采用的这样的 memory,那么平均访问时间就是 90% * 1 + 10% * (1 + 1 + 15 + 4 * 1) = 3 cycles

如果 block size 是 16 words,那么也是 90% * 1 + 10% * (1 + 1 + 15 + 16 * 1) = 4.2 cycles。相比 4 block 的情况,没有太大的问题。

Reduce Miss Rate

可以采用 multi-way 的方式来 reduce cache miss。

Cache Metric

忽略软件上的影响,我们只看硬件。

如果使用了 2-way,那么

  • miss rate 显然可以减少
  • 同时由于 2-way 的逻辑门增加了,因此时钟周期可能会更长一点

权衡利弊之后,如果只看 Average Memory Access Time 这个 metric,那么 2-way 总体上还是比 1-way 要略好一些。

但是,CPU 不仅有 memory access,还有每一条指令的执行。如果 clock cycle 减慢了,那么受影响的还有指令的执行。因此,加上这一点之后,1-way 还是比 2-way 更好一些。


因此,我们一般用 CPU time 来作为 cache metric。CPU time 的计算方法如下:

比如说,上面的 CPU TIME 1-way:

  • 2 * 1.0: CPU execution clock cycles per instruction
  • 1.5: 100% (imem access per instruction) + 50% (dmem access per instruction)
  • 1.5 * 0.014 * 75: Memory-stall clock cycles per instruction

另外,假如 block is dirty,那么我们还需要 write back to memory,这又是一次开销。在具体计算的时候,我们还需要加上。

Factors of Cache Metric

Cache metric 有几个 factors:

  • CPI: 比如使用多发射、超标量啥的技术,就可以将 CPI 降到 1,甚至比 1 还小
  • Clock Cycle Time: 现代处理器的主频不断提升,因此 clock cycle 一直在减小

但是,由于 memory 技术没怎么进步,因此,

  • 如果 CPI 下降了,Memory-stall clock cycle 并没有下降
  • 如果 Clock Cycle Time,由于内存访问时间不变,因此 Memory-stall clock cycles 反而会提升,从而更加拖后腿
    • 有可能 clock cycle time 降了一半,但是 CPU time 只是降到了原来的 0.81 倍,性能只提升到原来的 1.23 倍

Space Overhead of Cache

假设我们的 cache 是 1-way associative cache in 32-bit addressable space with

  • block size: 4 words = 16 bytes = \(2^4\) bytes
  • corresponding memory: 4 KB = \(2^{14}\) bytes

从而,我们的

  • block offset bits: 4
  • index bits: 14 - 4 = 10
  • tag bits: 32 - 10 - 4 = 18

从而,cache 实际上需要占用的空间大小是: $$ 2 ^ {10} * (16 + 18 / 8 + 1 / 8) \mathrm{~bytes} = 18.375 \mathrm{~KB} $$ 其中,

  • 16 是 block size
  • 18 和 1 分别是 tag bits and valid bit (i.e. 上图的 "V")
    • 后面除以 8 是为了将 bit 转换为 byte

Associativity of Cache

如果是 fully associative,使用硬件实现,一般就用三态门。但是硬件实现代价太高,而且会导致 clock cycle 大大减慢,因此一般不会这样做。

从而,我们一般用的是 set-associative。具体使用多少 set,还是需要权衡利弊。因为,对于总量相同的,如果采用更多的 set

  • 用于选择”具体取哪一个 set“的逻辑门电路增加,从而会导致 clock cycle 时间增加
  • 同样的内存容量,set 数量越多,统计上来看,冲突的概率就越小,因此 miss rate 会降低

Example

如果一个 cache 是:

  • 4K block size
  • each block contains 4 words
  • a word equals to 4 bytes
  • with byte-adressable memory and 32-bit address space

那么,自然,offset 就是 4 bits。但是,根据 associativity 的不同,index 和 tag 也会有所不同:

direct-mapped 2-way associative 4-way associative full associative
index bits 12 11 10 0
tag bits 16 17 18 28

另外,对于比较小的 associativity,大概就是这样实现的:

  • 对于上图中每一列,都检测是否 valid 且相等
  • 然后根据检测的结果,来输出 hit 以及 data
    • 输出 data 的时候,我们当然知道(正常情况下)4 个与门至多只有一个为真,因此是可以直接用三态门的。
    • 在这里,与其使用 mux,不如直接每一列的 32 bit 的 data 线,都和该列的与门并上,然后再放入一个 fan-in=4 的或门就完了。

Write Strategies of Cache

可以参考 csapp notes。这里的四种策略,其实要分为两组,每组两个。每一组分别对应 hit 和 miss 两种情况:

Cache 中已经有数据: Hit

  • Write through: 同时写到 cache 以及 memory
  • Write back: 只写到 cache,且 mark it as dirty;直到被替换了,才写进内存

Cache 中还没有数据: Miss

  • Write allocate: 分配一个 block(有必要可以替换),将 memory data fetch 进这个 block,然后再修改其中的对应的数据,并 mark as dirty
    • 既然数据已被修改,为何还需要 fetch original data into block?因为我们只修改 block 中的一小部分,并不是整个 block。未修改的部分,就是 original data
  • Non-Write allocate: 直接写进内存

不难发现:Write through 和 non-allocate 都是每一次都直接写入内存;而 write back 和 write allocate 就是能不写则不写。因此,"write through + non-allocate" 和 "write back + write allocate" 是相配的。

Inclusive, Exclusive and NI/NE Caching

这里假设只有 L1, L2 and memory.

Inclusive Cache(包容性缓存)

包容性缓存的主要特点是:L1 缓存中的数据地址也必须在 L2 缓存中存在(即 \(L_1 \subset L_2\),但是不要求数据内容一样。

  • 兼容性
  • Write Through: 包容性缓存适合 Write Through 策略,因为每次写操作都会直接写入主存储器,这与缓存层级之间的包容性保持一致。
  • Write Back: 包容性缓存与 Write Back 策略兼容,因为数据可以在 L1 中进行多次修改,最终会写回 L2 和主存储器。
  • Write Allocate: 包容性缓存支持 Write Allocate,因为新数据会分配到 L1 和 L2 缓存中。
  • Non-Write Allocate: 包容性缓存同样支持 Non-Write Allocate 策略,因为不需要将数据块加载到缓存中就可以直接写入主存储器。
  • Read and Flush:
  • Read 的时候,应该 L1 和 L2 均有一个备份
  • Flush 的时候,如果 L2 被 flush,那么 L1 的对应地址也应该被 invalidate

Exclusive Cache(独占性缓存)

独占性缓存的主要特点是:L1 缓存中的数据不在 L2 缓存中存在(即 \(L_1 \cap L_2 = \emptyset\)。L1 和 L2 缓存共同利用缓存空间,不会存储重复的数据块。

  • 兼容性
  • Write Through: 独占性缓存很不适合 Write Through 策略。原因很简单:write through 就是需要同时写 L1 和 L2 的,但是 exclusive 又要求同一个地址不能在这两块中同时出现。
    • i.e. L1 write hit 是,要同时写入 L1 和 L2,但是此处 L2 对应的地址,由 exclusive 可知,根本就不存在
  • Write Back: 独占性缓存与 Write Back 策略非常兼容,L1 中的数据在需要时才会写入 L2,充分利用缓存空间。
    • i.e. L1 write hit 是,只写入 L1,从而不会造成重复数据
  • Write Allocate: 独占性缓存支持 Write Allocate,因为新数据会分配到 L1 缓存中,腾出 L2 缓存空间给其他数据。
    • i.e. L1 write miss 时,会删除 L2 中的对应数据(如果存在),然后将该数据加载到 L1 中
  • Non-Write Allocate: 独占性缓存支持 Non-Write Allocate,因为它能够直接在主存储器中写入数据,而不需要占用缓存空间。
    • i.e. L1 write miss 时,直接写到 L2 去。不会造成重复数据。
  • Read and Flush:
  • Read 的时候,数据应该直接写到 L1 中,不应该经 L2 的手
  • Flush 的时候,如果 L1 被 flush,那么把 L1 的数据存到 L2
    • 这不是强制要求。只是这样做,能够在保证 exclusive 的同时,访问速度也有保证

NI/NE Cache(非包容性/非排他性缓存)

非包容性/非排他性缓存是一种灵活的缓存策略:L1 和 L2 缓存中的数据既可以重叠也可以独立存在。这种缓存策略不强制要求包容性或排他性。

  • 兼容性:肯定都是兼容的
  • Read and Flush: 策略可以非常随意

Replacement Strategies of Cache

一般而言,有 LRU (Least Recently Used) 和 FIFO 两种策略。

LRU

在 associativity 比较多的时候,LRU 的实现开销很大;而且 LRU 本身也不比随机算法好多少 (in practice)。

从而,我们一般采用树形结构近似

如上图:我们每一次访问一个节点的时候,就沿着树的路径,将所有树上的设成对应的值(0 代表左侧 more recently used,1 代表右侧 more recently used)。

比如说:如果访问了第 5 个块的时候,就会将

  • "2" 设为 1
  • "5" 设为 0
  • "6" 设为 1

然后,需要替换的时候,我们就沿着树,从树的根节点往下走:如果某个节点为 0,那就说明左侧 more recently used,因此就往右走;如果某个节点为 1,那就说明左侧 more recently used,因此就往左走。

FIFO

FIFO 就更简单了。给每一个 block 设置一个 "I'm the first" 的 bit 即可。

如果有 valid bit 为 0 的,那就直接写入,注意写入一定要按顺序,从 0 写到 \(2^n - 1\);如果所有 valid bit 均为 1,那么就将对应 bit vector 对应的 block 替换了,同时,每一个 block 都将自己的 "I'm the first" bit 设置成其左侧 block 的 "I'm the first" bit 即可。

原理:我们的插入是有序的,从而保证了:从 "I'm the first" bit 为 1 的 block 开始,被插入的时候向右递增。

  • 被插入时间最晚的 block,就是 "I'm the first" bit 为 1 的 block 左侧的 block。

Virtual Memory

我们并不会显式地在 memory 中额外划分一块,给 disk 当作 cache;而是通过虚拟内存的方式,将物理内存和硬盘上的内存有机结合了起来。

好处也很简单:

  • 每一个应用程序占用的虚拟内存是连续的,但是物理内存未必
  • 设置内存访问权限。有一些虚拟内存地址是 privilege 的
  • 不管物理内存的实际大小,我们有了一个固定、规律的寻址空间

在任意时刻,虚拟页面的集合都分为三个不相交的子集:

  • 未分配的 (Unallocated):VM 系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
  • 缓存的 (Cached):当前已缓存在物理内存中的已分配页。
  • 未缓存的 (Uncached):未缓存在物理内存中的已分配页。

TLE

由于页表是存在内存里的,因此如果没有缓存的话,就会比较慢。我们这里可以加一个 TLE,进行缓存。具体详见 csapp

简单来说,就是做下图中的替换:

image-20240203175759968

TLE Control Registers

其中:

  • EPC: 记录我们在哪里使用 lw, sw 指令产生了 TLB miss。这样好在取了 disk page 之后,再把这段指令执行完毕。
  • Cause: 不赘述
  • BadVAddr: 出问题的地址
  • Index: 需要在哪里读取 TLB 的 entry
  • Random: 就是一个随机数发生器。TLB 不采用 RLU 这样的策略,而是直接随机替换。
  • EntryLo/Hi: 由于 virtual addressable space 远比 physical addressable space 要大,因此 virtual address 的地址相关的信息需要用 2 个 word 来记录,也就是说,需要用到两个寄存器。
  • Context: 记录我们从内存中查到的 page table address 和 page number