Post

CS61C|Lec15-Caches

CS61C|Lec15-Caches

二进制前缀

  1. 常用前缀(SI和非SI)
  • Kilo, Mega, Giga, Tera, Peta, Exa, Zetta, Yotta:这些是常用的存储和数据传输前缀,表示不同的数量级。

  • SI值 vs. 常见用法:在计算机领域,有时这些前缀的用法和国际单位制(SI)有区别。例如:

  • “kilobyte”在常用中指的是1024字节(2^10 = 1,024),而SI标准的“kilobyte”应该是1000字节(10^3 = 1,000)。

  • 硬盘制造商电信业是少数遵循SI标准的领域,因此他们使用的是1000字节为单位,而不是1024字节。这导致硬盘标注的容量往往比用户期望的少。

  • 例如,标称1TB的硬盘实际上只有大约90%的预期容量。

  • 1 Mbit/s的连接速率实际传输速度是1,000,000 bps(基于SI标准的百万位)。

  1. 二进制前缀
  • 在计算机科学中,二进制系统使用的单位更常见,前缀的表示方法也不同,具体如下:

  • kibi (Ki) = 2^10 = 1,024

  • mebi (Mi) = 2^20 = 1,048,576

  • gibi (Gi) = 2^30 = 1,073,741,824

  • tebi (Ti) = 2^40 = 1,099,511,627,776

  • pebi (Pi) = 2^50 = 1,125,899,906,842,624

  • exbi (Ei) = 2^60 = 1,152,921,504,606,846,976

  • zebi (Zi) = 2^70 = 1,180,591,620,717,411,303,424

  • yobi (Yi) = 2^80 = 1,208,925,819,614,629,174,706,176

  • IEC标准:国际电工委员会(IEC)于1999年引入了这些前缀,以明确区分二进制和十进制的数量关系。前缀名称是由SI前缀的缩短形式加上”bi”(表示二进制)组成,但发音仍与SI前缀相同。

  1. 混淆来源
  • SI前缀的二义性:过去,SI前缀有时在计算机科学中既表示十进制也表示二进制。例如,“kilobyte”既可能表示1000字节,也可能表示1024字节。为了解决这一问题,IEC引入了二进制前缀,使SI前缀只表示十进制,而二进制前缀则明确表示二进制系统的单位。

速记

CS61C|Lec15-Caches-20250122.png

Cache的引入

因为高速发展的CPU速度和DRAM速度不匹配,导致DRAM拖了CPU的后腿,所以在CPU和内存中间,引入了一层”Cache”的概念。

Cache是主存的子集,包含着很小一部分主存里的数据。

将Cache类比成图书馆

  1. 有限的资源(缓存大小有限)
  • 类比你在桌子上放置大约10本书,而图书馆有成千上万本书。这就像缓存只能存储有限的数据,而计算机的主存储器或硬盘有非常庞大的数据量。

  • 缓存只能存储一小部分最常用的数据,就像你桌上的10本书是你认为最可能会用到的书。

  1. 从主存储获取数据(从主存储器中提取数据到缓存)
  • 类比中的“去图书馆查找相关书籍并放在桌子上”就像从主存储器或硬盘中获取数据并将其存储在缓存中。我去拿一次书,肯定会拿走可能要用上的书,这样就不用来回跑耽误时间了。

  • 如果你需要更多的书(数据),你会再次去取,而不是每次都重新从书架上取之前的书,这相当于缓存通过保留常用数据来避免频繁的主存储访问。

  1. 避免频繁返回(减少访问主存储器的频率)
  • 类比中说到,你不会轻易将之前借的书归还,因为你可能还会需要它们。这就像缓存系统保持最近使用的数据,以减少频繁从主存储器读取数据的需求。
  1. 小数据集足够(缓存加速访问)
  • 虽然你桌上只有10本书,但它们可能足够完成你的报告任务。这个类比解释了缓存可以显著加快数据访问速度,即使它只包含主存储中非常小的一部分数据。

Characteristics of the Memory Hierarchy

CS61C|Lec15-Caches-20250122-1.png

内存层次

  1. 核心思想(The Trick):
  • 目标:给处理器提供尽可能多的内存,同时使用最便宜的技术,并且以最快的速度访问数据。这是缓存和存储层次设计的主要原则。给处理器创造一种“内存既大又快”的幻觉。

  • 处理器希望从非常快的存储中获取数据,但同时需要在成本上进行优化,因此使用不同层次的存储设备来平衡速度和成本。

CS61C|Lec15-Caches-20250122-2.png

时间局部性和空间局部性

  1. 时间局部性(Temporal Locality):
  • 定义:如果一个数据在某个时间点被访问过,那么在不久的将来它很有可能会被再次访问。

  • 例子:在程序中,如果一个变量最近被读取或写入,那么这个变量很有可能在接下来的指令中再次被访问。因此,缓存会把这个变量存储起来,以便后续访问时不需要再次从主存储器读取。

  • 实际应用:CPU缓存将最近访问过的数据保留在缓存中,以提高后续的访问速度,避免频繁地从更慢的主存储器中读取数据。

  1. 空间局部性(Spatial Locality):
  • 定义:如果某个内存位置被访问了,那么与它相邻的内存位置很可能也会在不久的将来被访问。

  • 例子:在数组或连续内存数据结构中,当访问一个元素时,往往会紧接着访问其附近的元素。缓存会预取(prefetch)周围的数据块,以减少未来读取相邻数据时的延迟。

  • 实际应用:当程序访问某个内存地址时,缓存不仅仅存储该地址的数据,还会加载其相邻的内存块(数据块或缓存行),以提高后续访问相邻数据的效率。

直接映射Cache

直接映射中,每个内存地址都和一个块(block)相关联。因此,如果数据存在于缓存中,我们只需要在缓存中的一个位置查找数据。块是Cache和内存传输的单元。

CS61C|Lec15-Caches-20250122-3.png

  • 问题提出:如果我们希望缓存中的块大于1字节怎么办?

  • 例如,当块大小为2字节时,缓存每个位置能够存储两个连续的字节,而不是单个字节。这样,当我们请求一个字节时,整个块都会被加载到缓存中。

CS61C|Lec15-Caches-20250122-4.png

  • 当我们请求一个字节时,缓存控制器需要找到对应的块,并将整个块加载到缓存。

  • 如何找到正确的块?控制器使用内存地址来确定需要加载的块。内存地址被分为多个部分:一部分用于确定块的索引,另一部分用于在块内选择具体的字节。

  • 标记不需要包含整个内存地址,因为内存地址可以分解为索引块内偏移量标记

CS61C|Lec15-Caches-20250122-5.png

以下都是无符号整数

  1. 索引(Index):
  • 索引决定了我们应该在缓存的哪一行或哪个块中查找数据。缓存被分为多个块,每个块可以存储一定数量的数据,索引用于指定具体的缓存位置。

  • 比如,如果缓存有4个块,我们可以用2位的索引来表示这4个块的位置(如00、01、10、11)。

  1. 偏移量(Offset):
  • 偏移量用于指定在找到正确的缓存块后,具体需要读取块内的哪个字节。当块的大小大于1字节时,偏移量帮助我们选择所需的字节。

  • 例如,如果一个块有4个字节,我们需要用2位偏移量来决定块内的具体字节(如块内的第0字节、第1字节等)。

  1. 标记(Tag):
  • 标记是内存地址中的其余位,在确定了索引和偏移量后,标记用于区分映射到同一个缓存块的不同内存地址。

  • 多个内存地址可能映射到同一个缓存位置,但它们的标记会不同,因此标记帮助缓存控制器确定哪个内存地址对应于该缓存块中的数据。

CS61C|Lec15-Caches-20250122-6.png

例题

一个直接映射缓存(direct-mapped cache)的地址分解情况。缓存中有8字节的数据,块大小为2字节,问题要求确定标记(Tag)索引(Index)偏移量(Offset)字段的大小。让我们逐步分析:

  1. 偏移量(Offset):
  • 块大小为2字节:我们需要知道在每个块中具体的哪一个字节被访问。

  • 一个块有2字节,所以要选择这两个字节中的一个,我们需要1位来表示。即:

  • 0:选择块中的第一个字节

  • 1:选择块中的第二个字节

  • 偏移量大小 = 1位

  1. 索引(Index):
  • 缓存总大小为8字节,每个块2字节,这意味着总共有4个块(8 / 2 = 4块)。

  • 要选择4个块中的一个,我们需要log2(4) = 2位来表示索引位置。即:

  • 00:块0

  • 01:块1

  • 10:块2

  • 11:块3

  • 索引大小 = 2位

  1. 标记(Tag):
  • 一个地址在32位架构下有32位。

  • 地址可以分为:标记(Tag)索引(Index)和偏移量(Offset)。其中:

  • 偏移量已经占了1位。

  • 索引已经占了2位。

  • 因此,剩下的位数是标记部分,标记大小为32 - 1 - 2 = 29位。

CS61C|Lec15-Caches-20250122-7.png

没有缓存的内存访问:

在没有缓存的情况下,处理器需要直接从主存中读取数据,步骤如下:

  1. 处理器发出地址 1022(十进制)给内存。

  2. 内存读取位于地址 1022 的数据,假设内存中 Memory[1022] 的值为 99。

  3. 内存将数据 99 发送回处理器

  4. 处理器将 99 加载到寄存器 t0

这里每次内存访问都需要访问主存,速度较慢。

有缓存的内存访问:

有缓存时,缓存作为主存和处理器之间的中介,大大减少了访问主存的频率,步骤如下:

  1. 处理器发出地址 1022 给缓存,而不是直接访问内存。

  2. 缓存检查地址 1022 是否已经在缓存中

  • 命中(Hit):如果缓存中有地址 1022 对应的数据,缓存直接读取数据 99 并将其发送给处理器。

  • 未命中(Miss):如果缓存中没有地址 1022 的数据,缓存会将请求转发给主存:

  1. 主存读取地址 1022 的数据,即 99。

  2. 主存将数据 99 发送给缓存

  3. 缓存将读取的数据存储在自己的存储区域中,以备将来使用。

  4. 缓存将数据 99 发送给处理器

  5. 处理器将 99 加载到寄存器 t0

一些关于Cache的术语

在读取内存时,可能会发生以下三种情况:

  1. 缓存命中(Cache Hit)
  • 缓存块有效并且包含正确的地址,处理器能够直接从缓存中读取所需的数据。

  • 这意味着所请求的数据已经在缓存中,读取速度很快。

  1. 缓存未命中(Cache Miss)
  • 缓存中的相应块没有存储任何数据,处理器无法在缓存中找到所需的数据。

  • 缓存会从主存中获取数据,通常会导致较慢的访问。

  1. 缓存未命中且需要块替换(Cache Miss, Block Replacement)
  • 缓存的相应块中存储了错误的数据(即不是当前请求的数据)。

  • 缓存会丢弃这个错误的数据,并从主存中获取所需的数据,替换掉缓存中的数据。

  1. 命中率(Hit Rate)
  • 表示缓存访问时成功命中的次数占总访问次数的比例。

  • 也就是说,处理器从缓存中成功读取数据的频率。

  • 命中率越高,系统性能越好,因为处理器能够更多地从缓存中读取数据,而不是从较慢的主存中读取。

  1. 未命中率(Miss Rate)
  • 未命中率等于 1 - 命中率,表示缓存访问未命中的比例。

  • 这是处理器在缓存中找不到所需数据的次数。

  • 未命中率越低越好,因为未命中会导致系统不得不从较慢的主存中获取数据。

  1. 未命中惩罚(Miss Penalty)
  • 未命中时,从存储层次结构的下一级(如主存)中将数据替换到缓存所需的时间。

  • 通常来说,这个时间包括从主存读取数据、将数据加载到缓存中的过程。

  • 未命中惩罚越高,系统性能受到的影响就越大。

  1. 命中时间(Hit Time)
  • 访问缓存的时间,包括比较标签(Tag)的时间。

  • 这是处理器在缓存命中时,能快速访问数据的时间。

  • 缓存的命中时间通常很短,比访问主存的时间快得多。

  1. 缩写“$”表示缓存
  • 幻灯片提到,“$”是缓存(Cache)的缩写,这是一项伯克利大学的创新。

  • 在讨论缓存时,通常用“$”符号来代表缓存,如L1$(一级缓存)。

有效位

  1. 缓存初始化时的情况
  • 当启动一个新的程序时,缓存中不包含这个程序的有效数据。

  • 这意味着缓存中的数据可能是旧的、不相关的,因此需要有一种方法来判断缓存中的数据是否对当前程序有效。

  1. 有效位的作用
  • 为了解决这个问题,每个缓存标签条目中都会加入一个有效位(Valid Bit)

  • 有效位用来指示缓存中的条目是否包含当前程序的有效数据。

  • 有效位为 0:表示缓存条目无效,即使标签地址与所请求的地址匹配,也会被视为缓存未命中(Cache Miss),需要从主存中加载数据。

  • 有效位为 1:表示缓存条目有效,当处理器地址与标签匹配时,缓存命中(Cache Hit),可以从缓存中读取数据。

CS61C|Lec15-Caches-20250122-8.png

直接映射内存的例子

16KB大小的直接映射缓存,4个字为一个块,也就是宽度。所以我们简单计算一下,16KB是$2^{14}$,4个字是16Bytes,需要$2^{4}$,也就是4位来索引Byte,$2^{14-4}$ = $2^{10}$,所以需要10位来索引行

CS61C|Lec15-Caches-20250122-9.png

接下来我们将地址转换成TIO的形式

CS61C|Lec15-Caches-20250122-10.png

先来看第一个地址,首先根据索引找到对应的行,但是发现有效位是0,所以需要从主存中将数据传输过来。

CS61C|Lec15-Caches-20250122-11.png

传输过来之后,将有效位设置为1,并根据偏移量找到对应的Byte。

CS61C|Lec15-Caches-20250122-12.png

找第二个地址的数据的时候,就命中缓存了,直接拿走即可。

CS61C|Lec15-Caches-20250122-13.png

第三个地址的以此类推,我们来讲第四个地址发现Tag不匹配的情况。

CS61C|Lec15-Caches-20250122-14.png

不匹配就重新从主存中拿数据,就可以了。

CS61C|Lec15-Caches-20250122-15.png

学习至此的困惑

“如果CPU想要寻址一个地址,它是不是先在缓存中查找,如果找不到,缓存再从主存中读取?”

没错,CPU 在需要访问数据时,会首先检查高速缓存(Cache)中是否存在所需的数据。这个过程称为缓存命中(Cache Hit)。如果在缓存中找到了所需数据,则直接从缓存读取;如果没有找到(称为缓存未命中或 Cache Miss),则会到主存储器(Main Memory)中去寻找,并且通常会将找到的数据的一部分复制到缓存中,以便后续更快地访问。

至于地址格式的问题,实际上无论是缓存还是主存,它们使用的是相同的地址空间。也就是说,CPU 产生的地址是一个虚拟地址(在使用虚拟内存系统的情况下),这个地址可以直接用于缓存的索引,也可以用于访问主存。这是因为缓存系统设计时考虑到了与主存的一致性。在缓存中,使用的是与主存相同的地址格式来索引和组织其内部存储单元。

多字作为块的直接映射

CS61C|Lec15-Caches-20250122-16.png

处理写操作

在课上介绍了两种写回策略,但是在软硬件接口这本书中提到了三种。

  1. 写穿透/写直达(write-through):总是将数据写回到Cache和主存,优点是保证了数据的一致性,缺点是每次都要进行写主存的操作,性能不好。

  2. 写返回(write-back):发生写操作时,新值只被写入cache中,当该数据块被替换的时候,才将更新后的数据块写入下一级储存。

    • 只更新缓存:在write-back策略中,写操作只更新缓存中的数据,而不是立即更新主内存。

    • 内存数据可能“过时”:由于写操作只更新缓存,主内存的数据可能保持为旧数据(或称“过时”数据)。

    • “Dirty bit”(脏位):为了跟踪缓存块是否被修改,每个缓存块会有一个“dirty bit”(脏位)。如果该位被设置为1,表示缓存中的数据与内存不同步,需要在缓存块替换时更新主内存。

    • 内存与缓存不一致:由于write-back策略下主内存不会立即更新,所以内存和缓存的数据可能不一致。这在数据被替换时需要特别注意。

    • 操作系统在I/O前刷新缓存:为了避免I/O操作中数据不一致的风险,操作系统通常会在进行I/O操作之前先将缓存的数据刷新到主内存。

  3. 写缓存(write buffer)

关于块大小的权衡

块大小的特点

较大的块大小的优点包括:

  • 空间局部性:访问一个单词后,很可能会访问附近的其他单词

  • 适用于存储程序概念

  • 适用于顺序数组访问

但是,较大的块大小也有一些缺点:

  • 缺失惩罚增加:当缓存未命中时,加载新块需要更长的时间

  • 如果块大小过大,缓存中块的数量会减少,导致缓存未命中率增加

举个例子

缓存大小为4bytes,和块大小相等,这导致缓存里面只有一行

CS61C|Lec15-Caches-20250122-17.png

如果一个项目被访问了,那么很可能很快会再次被访问,

  • 但不太可能会立即再次访问!

  • 下一次访问很可能还是会错失(缓存未命中)。

  • 不断将数据加载到缓存中,但在使用数据之前将其丢弃(强制移出)。

  • 对于缓存设计师来说,这是个噩梦:乒乓效应。

CS61C|Lec15-Caches-20250122-18.png

缓存未命中

缓存未命中的种类

  1. 强制未命中(Compulsory Misses)
  • 当程序第一次运行时发生。

  • 缓存中还没有该程序的数据,所以未命中是必然的。

  • 这种未命中很难避免,因此在本课程中不会重点讨论。

  • 每个内存块都会经历一次强制未命中(不仅仅是每个缓存块)。

  1. 冲突未命中(Conflict Misses)
  • 这种未命中发生在两个不同的内存地址映射到相同的缓存位置时。

  • 当两个内存块(恰好映射到同一个位置)相互覆盖时会导致冲突未命中。

  • 这是直接映射缓存中的一个大问题。

解决方案 1:增大缓存大小

  • 这种方法能在一定程度上缓解问题,但最终会失效。

解决方案 2:允许多个不同的块适应相同的缓存索引

  • 这可以通过增加缓存的组关联度(associativity)来实现,从而减少冲突未命中的可能性。这就引入了全相连映射缓存的概念了。

全相连映射缓存(Fully Associative Caches)

内存地址字段:

  • 标记(Tag): 和之前一样,用来识别缓存中的数据块。

  • 偏移量(Offset): 和之前一样,用于定位块内的数据。

  • 索引(Index): 不存在。

这意味着什么?

  • 没有“行”: 任何块可以存放在缓存的任何位置。

  • 必须比较整个缓存中的所有标记: 为了确认数据是否在缓存中,必须将内存地址的标记与缓存中所有块的标记进行比较。

CS61C|Lec15-Caches-20250122-19.png

优点:不会出现冲突未命中,因为数据可以去到任何地方,不是固定非要去某一个block

缺点:复杂的硬件实现,如果我们在缓存中有64KB的数据,且每个条目为4B,那么我们需要16K个比较器:这不可行。

最后一种缓存未命中类型——容量未命中

  • 什么是容量未命中?

  • 当缓存的大小有限时,发生的未命中现象。

  • 如果缓存的大小足够大,这种未命中本来是可以避免的。

  • 这是个相对不精确的定义,但你只需要理解基本概念即可。

  • 关键点:

  • 由于缓存空间不足,当新的数据块需要加载时,已有的数据块可能被移出缓存。

  • 如果后续又需要访问已移出的数据块,缓存将未命中,这就是典型的容量未命中

对于全相联缓存,容量未命中是主要的未命中类型。

  • 全相联缓存允许任何块放在任何位置,因此冲突未命中的概率很低,但由于缓存容量有限,依然会因为空间不足而导致未命中,即容量未命中。

如何将缓存未命中分类

这就是区分未命中到底是哪种类型的方法,但是现阶段我并没有对此有太好的理解。

CS61C|Lec15-Caches-20250122-20.png

N路组相连缓存

N 路组相联缓存 中,内存被分为多个(set),每组包含多个(block)。我们用主存地址的某些字段来找到正确的组,然后在该组中的所有块中比较标签(Tag)来找到数据。

基本思想

在N路组相联缓存中,缓存被组织成多个“组”或“集”,每个组包含N个缓存行或块。这种结构使得每个组内部是全相联的,而整个缓存则是直接映射到这些组上的。这意味着对于任何一个内存地址来说,它只能映射到一个特定的组上,但是在该组内,它可以落在任何一个缓存行上。

访问流程

  1. 查找组别:使用内存地址中的索引部分来确定应该访问哪个组。
  2. 比较标签:将内存地址中的标签部分与该组中所有行的标签进行比较。
  3. 命中或未命中:如果有一个匹配的标签,则表示命中;如果没有匹配,则表示未命中。
  4. 使用偏移量:如果命中,使用地址中的偏移量来定位该块内的具体数据。

优点

减少冲突未命中

  • 即使是一个2路组相联缓存也能够避免大量的冲突未命中。这是因为每个组有多个块,所以即使两个不同的内存地址映射到了同一个组,它们也可能不会总是映射到同一块上,从而减少了冲突的机会。

较低的硬件成本

  • 实现N路组相联缓存只需要N个比较器来同时检查组内所有块的标签是否匹配,因此相比于完全相联缓存,其硬件成本较低。

特殊情况

对于一个具有M个块的缓存:

  • 如果它是1路组相联,那么它实际上就是一个直接映射的缓存。

  • 如果它是M路组相联,那么它就是一个全相联缓存。

关键概念:

  1. Tag(标签):标签用于唯一标识内存块,帮助我们在缓存中定位数据。

  2. Index(索引):索引字段告诉我们要访问的组(即缓存的“行”),然后在这个组内进行查找。

  3. Offset(偏移):偏移字段用于定位组内块的具体字节,帮助精确找到数据。

区别在哪?

与直接映射缓存不同的是,组相联缓存中的每个组有多个块。这样,当找到正确的组后,我们需要对组内所有块的标签进行比较,看看哪个块包含所需的数据。

举例:

在一个 2 路组相联缓存中,每个组有 2 个块。如果某个数据块在缓存中,它可以存储在任意两个块中的一个,而不是被固定映射到一个位置。

CS61C|Lec15-Caches-20250122-21.png

总结:

  • Direct-Mapped Cache(直接映射缓存):每个组只有 1 个块(1 路),所以不需要比较多个块的标签。

  • Fully Associative Cache(全相联缓存):只有 1 组,包含所有缓存块,所以所有块的标签都要比较。

  • N-Way Set Associative Cache:是介于这两者之间的一种设计。它既避免了直接映射缓存的冲突缺失(conflict miss),硬件开销也相对较低,只需要 N 个比较器。

CS61C|Lec15-Caches-20250122-22.png

块替换

块替换策略基本概念

在缓存满的情况下,新的数据块需要替换掉一个旧的块。具体选择哪个块,这由块替换策略决定。

直接映射缓存(Direct-Mapped Cache)

  • 块的位置由索引字段唯一确定,未命中时,只能写入该索引指定的固定位置。

N 路组相联缓存(N-Way Set Associative Cache)

  • 索引字段决定块所在的组,但组内的任何位置都可以存放新的块。未命中时,如果该组满了,需要用替换策略来决定要替换哪个块。

全相联缓存(Fully Associative Cache)

  • 块可以写入缓存中的任何位置,替换策略决定写入哪个块。

问题来了:新的块应该被放到哪里去呢?

如果有空位,那就写入第一个空位(有效位被设置成无效的),如果所有行都是有效的,那我们就得选择一个替换策略来替换了。

块替换策略

  1. LRU(Least Recently Used,最近最少使用)
  • 替换最近未被访问的块。

  • 优点:利用时间局部性,即最近使用的数据块可能在未来继续被使用。

  • 缺点:在 2 路组相联缓存中容易实现,但在更高路的缓存中,跟踪最近最少使用的块需要更复杂的硬件和更多时间。

  1. FIFO(First In, First Out,先进先出)
  • 替换最早进入缓存的块,忽略块是否被访问过。
  1. 随机(Random)
  • 随机选择一个块进行替换,在某些情况下,如时间局部性较低的工作负载,随机替换策略也能有效工作。

块替换的例子

我们有一个相同的2路组相联缓存,总容量为4字节,块大小为1字节。我们进行以下字节访问:0, 2, 0, 1, 4, 0, 2, 3, 5, 4

CS61C|Lec15-Caches-20250122-23.png

CS61C|Lec15-Caches-20250122-24.png

平均内存访问时间

重要思想

AMAT 是衡量缓存系统性能的关键指标,它的公式为

$Average Memory Access Time = Hit Time + Miss Penalty x Miss Rate$

改进未命中惩罚

  1. Miss Penalty 的变化
  • 过去的情况:在缓存首次广泛使用时,丢失惩罚(Miss Penalty)大约是 10个处理器时钟周期。这意味着,当缓存未命中时,处理器需要等待约10个时钟周期来获取所需数据。

  • 现在的情况:随着处理器速度的提高,处理器和主存(DRAM)之间的速度差距越来越大。如今,主流处理器的时钟速度为 3 GHz(即每个时钟周期约 1/3 纳秒),而从DRAM读取数据则需要 80纳秒。因此,访问DRAM的时间约为 200个时钟周期

为什么丢失惩罚增加了这么多?

  • 处理器速度的提升:现代处理器变得非常快,每秒钟能执行数十亿个指令(例如3 GHz的处理器每秒可执行30亿个时钟周期)。然而,内存的速度提升幅度远不及处理器,因此访问DRAM的时间相对处理器变得极其缓慢。

  • DRAM的相对延迟增加:虽然内存速度也有所提升,但与处理器速度相比,延迟显得更大。这导致了现代系统中,处理器在缓存未命中时等待的时间变得非常长(约200个时钟周期)。

  1. L2缓存的引入

为了应对这个巨大的延迟,计算机体系结构引入了 L2缓存,即 二级缓存。L2缓存的位置在 处理器的L1缓存和主存(DRAM)之间,主要用于减少L1缓存未命中时直接访问主存的高延迟。

L2缓存的好处:

  • 减少丢失惩罚(Miss Penalty):L2缓存的存在大幅减少了访问主存的频率。与L1缓存相比,L2缓存的容量较大,虽然访问速度稍慢(多个时钟周期),但仍远快于直接访问DRAM。因此,它能够有效地降低L1缓存未命中的丢失惩罚。

  • 减轻主存的压力:如果没有L2缓存,L1缓存未命中时处理器需要频繁地访问主存,这会造成严重的性能瓶颈。L2缓存作为中间层,存储更多的缓存行,减少了主存的访问压力。

  1. 现代系统中的多级缓存层次

CS61C|Lec15-Caches-20250122-25.png

随着系统复杂性的提升,多级缓存层次变得非常普遍:

  • L1缓存:速度最快、容量最小,通常用于存储最频繁使用的数据,命中时间非常短(1个时钟周期)。

  • L2缓存:容量更大但稍慢,作为L1未命中的“后备”缓存,帮助减少对主存的访问。

  • L3缓存:有时存在于多核处理器中,用于多个核心共享数据,进一步减少主存访问。

CS61C|Lec15-Caches-20250122-26.png

减少未命中率的方法

  1. 增大缓存容量
  • 被技术和成本所限制

  • 一级缓存命中时间 < 时钟周期(缓存越大越慢)

  1. 采用相连映射缓存
  • 全相连

  • 组相连

    多级缓存的一般规模

缓存通常分为多个层次,主要是L1和L2缓存。

L1缓存:

  • 容量较小,通常为数十KB

  • 访问速度最快,一个时钟周期内就能完成

  • miss率只有1-5%

L2缓存:

  • 容量较大,通常为数百KB

  • 访问速度较L1慢一些,需要几个时钟周期

  • miss率为10-20%(这里指的是L1 miss后再访问L2的miss率)

L2 miss率为何这么高?

因为L1命中了很多,L2只是在处理L1没处理的部分

L2缓存面临的访问请求本身就已经是“更难处理”的部分,这些请求有更高的概率在L2中也找不到数据,所以L2的未命中率会比L1高。这并不意味着L2的设计不好,而是因为它负责的是更复杂的任务,即处理L1缓存未能命中的情况。

例子

示例1: 使用L2缓存的情况

假设:

  • L1缓存命中时间 = 1个时钟周期

  • L1未命中率 = 5%

  • L2缓存命中时间 = 5个时钟周期

  • L2未命中率 = 15%(即L1未命中的情况下,L2也未命中的概率)

  • L2未命中惩罚 = 200个时钟周期

那么,L1未命中的惩罚时间: $L1 miss penalty = 5 + 0.15 \times 200 = 35$

平均内存访问时间: $Avg mem access time = 1 + 0.05 \times 35 = 2.75 cycles$

示例2: 不使用L2缓存的情况

假设:

  • L1缓存命中时间 = 1个时钟周期

  • L1未命中率 = 5%

  • L1未命中惩罚 = 200个时钟周期

那么,平均内存访问时间: $Avg mem access time = 1 + 0.05 \times 200 = 11 cycles$

This post is licensed under CC BY 4.0 by the author.