Cache工作流程

Cache出现的目的是为了弥补CPU与内存之间速度不匹配的状况,如果要访问的内存地址的内容已经存在于Cache中,那么CPU就无需访问速度较慢的内存。本文通过一个内存访问的例子,简单介绍两种Cache模型,以及它们是如何工作的。

Cache Line

Cache的理论基础是程序的局部性原理,当访问了某个不在Cache中的内存地址后,通过将相关内存地址的数据迁移到Cache中,降低再次需要访问内存的概率。

从内存向Cache迁移数据是以Line Size为基础单位,假如Line Size是32Byte,那么每次迁移都是32Byte。

当给定一个内存地址时,我们怎样让其与Cache Line中的某个具体Byte对应呢?参照内存分页机制,不妨也将内存地址拆分为几组,这就引申出了Set,Tag和Offset三个概念。

整个Cache空间被分为若干个Set,每个Set包含若干个Cache Line,每个Cache Line用Tag标识,当找到对应的Cache Line后,使用Offset来确定在Cache Line中的偏移。

根据每个Set中Cache Line的数量,Cache可以分为直接映射组相联全相联三种。

Direct Mapped

在直接映射模式中,每个Set只有1个Cache Line,假设我们的Cache有128Byte,每个Cache Line有32Byte,内存地址是12bit(通常来说是32bit或者64bit,为简单起见使用12bit),那么

Offset = log(32) = 5bit,即使用5bit作为在Cache Line中的索引

Set = log(128/32/1) = log(4) = 2bit,其中1表示每个Set有1个Cache Line

Tag = 12 - 5 -2 = 5bit,剩下的bit用于标识Cache Line

即内存地址被分割为

Addr Tag Set Offset
12bit 5bit 2bit 5bit
0x070 00000 11 10000

对应的Cache空间布局如下表所示

Set Valid Tag (Line)
00 0 0
01 0 0
10 0 0
11 0 0

当我们在访问内存时,首先把内存地址划分为Tag、Set和Offset,然后根据Set选择对应的组,如果Valid字段为1,并且Tag字段匹配,那么Cache命中,否则Cache未命中,需要把内存中1 Line的数据存储到对应的Cache Line。

假如我们有一组待访问的内存地址,那么第一次访问以及第二次访问的命中情况如下表所示。

Addr Tag Set Offset 1st 2nd
0x070 00000 11 10000 M H
0x080 00001 00 00000 M M
0x068 00000 11 01000 H H
0x190 00011 00 10000 M M
0x084 00001 00 00100 M M
0x178 00010 11 11000 M M
0x08c 00001 00 01100 H H
0xf00 11110 00 00000 M M
0x064 00000 11 00100 M M

第一次访问Cache的命中率是:2/9 = 22%

第二次访问Cache的命中率是:2/9 = 33%

2-way Set Associative

在2路组相联模式中,每个Set有2个Cache Line,假设我们的Cache有128Byte,每个Cache Line有32Byte,内存地址是12bit(通常来说是32bit或者64bit,为简单起见使用12bit),那么

Offset = log(32) = 5bit,即使用5bit作为在Cache Line中的索引

Set = log(128/32/2) = log(2) = 1bit,其中2表示每个Set有2个Cache Line

Tag = 12 - 5 -1 = 6bit,剩下的bit用于标识Cache Line

即内存地址被分割为

Addr Tag Set Offset
12bit 6bit 1bit 6bit
0x070 000001 1 10000

对应的Cache空间布局如下表所示

Way 1 Way 0
Set LRU Valid Tag (Line) Valid Tag (Line)
0 0 0 0 0 0
1 0 0 0 0 0

注意,由于此时每个Set中有2个Cache Line,那么当Cache未命中时,应该替换哪一个Cache Line呢?这时候就有多种策略可以选择,比较常见的是LRU,即最近最少使用算法。比如我们刚更新过Line0,那么LRU就被设置为1,表示Line1将在下一次未命中发生时被替换。本意是将最不活跃的Line替换出去。

那么,使用同样一组数据,Cache命中情况如下表

Addr Tag Set Offset 1st 2nd
0x070 000001 1 10000 M H
0x080 000010 0 00000 M H
0x068 000001 1 01000 H H
0x190 000110 0 10000 M M
0x084 000010 0 00100 H H
0x178 000101 1 11000 M H
0x08c 000010 0 01100 H H
0xf00 111100 0 00000 M M
0x064 000001 1 00100 H H

第一次访问Cache的命中率是:4/9 = 44%

第二次访问Cache的命中率是:7/9 = 77%

可以看出,在没有增大Cache空间的情况下,仅仅改变了Cache的模式,就大幅提高了命中率。

Fully Associative

在全相联模式下,只有1个Set,假设我们的Cache有128Byte,每个Cache Line有32Byte,那么这个Set中将包含4个Cache Line。

Way = 128/32 = 4

对于小空间的Cache来说,似乎不是什么问题,但是当Cache有几百KB甚至几MB,那么就会存在大量的Cache Line,这会造成两个问题

  1. 由于是通过比较Tag来确定对应的Cache Line,而不是使用索引,比较这么多Tag很低效
  2. LRU算法需要找到最长时间未被使用的Cache Line,而这一般是硬件完成的,如果Cache Line很多,硬件设计会变得很复杂

因此,几乎没有实际的硬件使用这种Cache模式。

总结

本文简单的介绍了一下Cache模型,以及其工作方式,虽然Cache对于程序员来说是透明,但了解其工作方式可以帮助我们更好的优化代码性能。如果想了解文中例子的具体执行流程,可以观看参考中的两个视频。

参考

Cache Access Example (Part 1)

Cache Access Example (Part 2)