Appendix B Review of Memory Hierarchy

本文复习了内存层次结构,重点介绍了缓存的工作原理,包括缓存命中与未命中、性能方程和优化策略。讨论了缓存的类型(如直接映射、全关联和组关联)、替换策略(随机、LRU和FIFO)以及写操作的影响。通过AMD Opteron微处理器的数据缓存实例,阐述了缓存设计的细节,强调了指令和数据缓存分开的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本附录是对内存层次结构的快速复习,包括缓存和虚拟内存、性能方程和简单的优化。 这第一部分回顾了以下 36 个术语:
在这里插入图片描述
如果此评论进行得太快,您可能需要查看计算机组织和设计中的第 7 章,这是我们为经验较少的读者编写的。

缓存是地址离开处理器后所遇到的最高或第一级内存层次结构的名称。 因为局部性原则适用于许多级别,并且利用局部性来提高性能很流行,所以现在只要使用缓冲来重用常见的项目,就会应用术语缓存。 示例包括文件缓存、名称缓存等。

当处理器在缓存中找到请求的数据项时,称为缓存命中。 当处理器在缓存中找不到它需要的数据项时,就会发生缓存未命中。 包含所请求字的固定大小的数据集合,称为块或行运行,从主存储器中检索并放入缓存中。时间局部性告诉我们,在不久的将来我们可能会再次需要这个词,因此将它放在可以快速访问的缓存中很有用。由于空间局部性,很可能很快就会需要块中的其他数据。

缓存未命中所需的时间取决于内存的延迟和带宽。 延迟决定了检索块第一个字的时间,带宽决定了检索该块其余部分的时间。缓存未命中由硬件处理并导致使用有序执行的处理器暂停或停止,直到数据可用。 对于乱序执行,使用结果的指令仍必须等待,但其他指令可能会在未命中期间继续执行。

同样,并非程序引用的所有对象都需要驻留在主内存中。 虚拟内存意味着一些对象可能驻留在磁盘上。 地址空间通常分为固定大小的块,称为页。 在任何时候,每个页面都驻留在主内存或磁盘上。 当处理器引用缓存或主内存中不存在的页面中的项目时,会发生 palt,整个页面从磁盘移动到主内存。 因为页面错误需要很长时间,所以它们在软件中处理并且处理器不会停止。 当磁盘访问发生时,处理器通常会切换到其他一些任务。 从高层次的角度来看,对引用局部性的依赖以及缓存与主内存的大小和相对成本的相对关系与主内存与磁盘的相关性相似。
在这里插入图片描述
图 B.1 显示了从高端台式机到低端服务器的计算机的内存层次结构中每个级别的大小范围和访问时间。

Cache Performance Review

由于较小内存的局部性和更高的速度,内存层次结构可以显着提高性能。 评估缓存性能的一种方法是扩展第 1 章中的处理器执行时间方程。我们现在考虑处理器停滞等待内存访问的周期数,我们称之为内存停滞周期。 性能是时钟周期时间与处理器周期和内存停顿周期之和的乘积:
在这里插入图片描述
此等式假设 CPU 时钟周期包括处理高速缓存命中的时间,并且处理器在高速缓存未命中期间停顿。 B.2 节重新审视了这个简化的假设。

内存停顿周期数取决于未命中数和每次未命中的成本,称为未命中惩罚:
在这里插入图片描述
最后一种形式的优点是可以轻松测量组件。 我们已经知道如何测量指令数 (IC)。 (对于推测处理器,我们只计算提交的指令。)可以用同样的方式测量每条指令的内存引用数; 每条指令都需要指令访问,并且很容易决定它是否也需要数据访问。

请注意,我们将未命中惩罚计算为平均值,但我们将在此处使用它,就好像它是一个常数一样。 由于先前的内存请求或内存刷新,缓存后面的内存可能在未命中时很忙。 处理器、总线和存储器的不同时钟之间的接口处的时钟周期数也不同。 因此,请记住,使用单个数字进行未命中惩罚是一种简化。

组件未命中率只是导致未命中的缓存访问的分数(即未命中的访问次数除以访问次数)。 可以使用高速缓存模拟器测量未命中率,该模拟器获取指令和数据引用的地址跟踪,模拟高速缓存行为以确定哪些引用命中和哪些未命中,然后报告命中和未命中总数。 今天许多微处理器提供硬件来计算未命中和内存引用的数量,这是一种更容易和更快的测量未命中率的方法。

前面的公式是一个近似值,因为读取和写入的未命中率和未命中惩罚通常不同。 然后可以根据每条指令的内存访问次数、读取和写入的未命中惩罚(以时钟周期为单位)以及读取和写入的未命中率来定义内存停顿时钟周期:
在这里插入图片描述
我们通常通过组合读取和写入并找到读取和写入的平均未命中率和未命中惩罚来简化完整公式:
在这里插入图片描述
未命中率是缓存设计最重要的衡量标准之一,但正如我们将在后面的部分中看到的,它不是唯一的衡量标准。

一些设计人员更喜欢将未命中率测量为每条指令的未命中数,而不是每个内存引用的未命中数。 这两个是相关的:
在这里插入图片描述
当您知道每条指令的平均内存访问次数时,后一个公式很有用,因为它允许您将未命中率转换为每条指令的未命中,反之亦然。 例如,我们可以将以下示例中的每个内存引用的未命中率转换为每条指令的未命中率:
在这里插入图片描述

在这里插入图片描述
顺便说一下,每条指令未命中通常报告为每 1000 条指令未命中,以显示整数而不是分数。 因此,前面的答案也可以表示为每 1000 条指令 30 次未命中。

每条指令未命中的优点是它独立于硬件实现。 例如,推测处理器获取的指令数量大约是实际提交的指令数量的两倍,如果以每个内存引用而不是每个指令的未命中来衡量,这可以人为地降低未命中率。 缺点是每条指令的缺失取决于体系结构; 例如,对于 80x86 与 RISC V,每条指令的平均内存访问次数可能会有很大不同。 因此,每条指令的未命中在使用单个计算机系列的架构师中最为流行,尽管 RISC 架构的相似性允许人们和其他人对比。

Four Memory Hierarchy Questions

我们通过回答内存层次结构第一级的四个常见问题来继续介绍缓存
在这里插入图片描述
这些问题的答案有助于我们理解在不同层次的层次上的记忆的不同权衡; 因此,我们对每个示例都提出这四个问题。

Q1: Where Can a Block be Placed in a Cache?

图 B.2 显示了对块放置位置的限制创建了三类缓存组织:
在这里插入图片描述
图B.2 This example cache has eight block frames and memory has 32 blocks. 缓存的三个选项从左到右显示。 在完全关联的情况下,来自较低级别的块 12 可以进入缓存的八个块帧中的任何一个。 对于直接映射,块 12 只能放入块帧 4(12 模 8)中。 Set associative 具有这两种功能中的一些,允许将块放置在 set 0(12 模 4)中的任何位置。 每组两个块,这意味着块 12 可以放置在缓存的块 0 或块 1 中。 真实缓存包含数千个块帧,而真实内存包含数百万个块。 集合关联组织有四个集合,每个集合有两个块,称为双向集合关联。 假设缓存中没有任何内容,并且所讨论的块地址标识较低级别的块 12。

  • 如果每个块只有一个地方可以出现在缓存中,则称缓存是直接映射的。 映射通常是 在这里插入图片描述
  • 如果块可以放置在缓存中的任何位置,则称该缓存是完全关联的。
  • 如果一个块可以放置在缓存中的一组受限位置中,则缓存设置为关联的。 集合是缓存中的一组块。 一个块首先被映射到一个集合,然后该块可以放置在该集合内的任何位置。 该集合通常通过位选择来选择; 那是,
    在这里插入图片描述
    如果集合中有 n 个块,则缓存放置称为 n 路集合关联。
    从直接映射到完全关联的缓存范围实际上是集合关联性级别的连续统。 直接映射只是单向集合关联,具有 m 个块的完全关联缓存可以称为“m 路集合关联”。 等价地,直接映射可以被认为具有 m 个集合,而完全关联的可以被认为具有一个集合。

当今绝大多数处理器缓存都是直接映射、二路组关联或四路组关联,原因我们将很快看到。

Q2: How Is a Block Found If It Is in the Cache?

缓存在每个块帧上都有一个地址标签,用于提供块地址。 检查可能包含所需信息的每个缓存块的标记,以查看它是否与来自处理器的块地址匹配。 通常,并行搜索所有可能的标签,因为速度至关重要。 必须有办法知道缓存块没有有效信息。 最常见的过程是向标签添加一个有效位,以说明该条目是否包含有效地址。 如果未设置该位,则此地址无法匹配。

在这里插入图片描述
在继续下一个问题之前,让我们探讨一下处理器地址与缓存的关系。 图 B.3 显示了地址是如何划分的。 第一个划分是在块地址和块偏移之间。 块帧地址可以进一步分为标签字段和索引字段。 块偏移字段从块中选择所需的数据,索引字段选择集合,标签字段与它进行比较以获得命中。 虽然可以在比标签更多的地址上进行比较,但没有必要,因为以下几点:

  • 在比较中不应使用偏移量无论整个块是否存在,因为根据定义,所有块偏移都会导致匹配。
  • 检查索引是多余的,因为它用于选择要检查的集合。 例如,存储在集合 0 中的地址必须在索引字段中为 0,否则无法存储在集合 0 中; set 1 的索引值必须为 1; 等等。 此优化通过减少缓存标签的内存大小宽度来节省硬件和电源。

如果总缓存大小保持不变,增加关联性会增加每个集合的块数,从而减少索引的大小并增加标签的大小。 也就是说,图 B.3 中的标签-索引边界随着关联性的增加向右移动,完全关联缓存的端点没有索引字段。

Q3: Which Block Should be Replaced on a Cache Miss?

当发生未命中时,缓存控制器必须选择要替换为所需数据的块。 直接映射布局的一个好处是简化了硬件决策——事实上,如此简单以至于没有选择:只检查一个块帧是否命中,并且只有该块可以被替换。 通过完全关联或设置关联放置,有许多块可供选择。 选择要替换的块时采用了三种主要策略:

  • 随机 - 为了均匀分布分配,随机选择候选块。 一些系统生成伪随机块号以获得可重现的行为,这在调试硬件时特别有用。
  • 最近最少使用(LRU)——为了减少丢弃很快需要的信息的机会,对块的访问被记录下来。 依靠过去来预测未来,被替换的块是最长时间未被使用的块。 LRU 依赖于局部性的一个推论:如果最近使用的块很可能会再次使用,那么一个很好的处理候选者是最近最少使用的块。
  • 先进先出 (FIFO)——因为 LRU 计算起来很复杂,这通过确定最旧的块而不是 LRU 来近似 LRU。

随机替换的一个优点是可以简单地构建硬件。随着要跟踪的块数量的增加,LRU 变得越来越昂贵并且通常只是近似值。一个常见的近似(通常称为pseudoLRU)在缓存中为每个集合都有一组位,每个位对应一个单路(一个路是组关联缓存中的组;四路组关联缓存中有四个路)在缓存中。当一个集合被访问时,对应于包含所需块的路的位被打开;如果与一组关联的所有位都打开,则除了最近打开的位之外,它们都将复位。当一个块必须被替换时,处理器从其位被关闭的方式中选择一个块,如果有多个选择可用,通常是随机的。这近似于 LRU,因为自上次访问集合中的所有块以来,将不会访问被替换的块。图 B.4 显示了 LRU、随机和 FIFO 替换之间的未命中率差异。
在这里插入图片描述
Q4: What Happens on a Write?

读取控制处理器缓存访问。 所有指令访问都是读取,并且大多数指令不会写入内存。 附录 A 中的图 A.32 和 A.33 建议 RISC V 程序混合使用 10% 存储和 26% 加载,使写入占总内存的 10%/(100%+26%+10%) 或大约 7% 交通。 在数据缓存流量中,写入是 10%/(26%+10%) 或大约 28%。 使常见情况快速意味着优化读取缓存,特别是因为处理器传统上等待读取完成但不需要等待写入。 然而,阿姆达尔定律(第 1.9 节)提醒我们,高性能设计不能忽视写入速度。

幸运的是,常见的案例也是容易快速制作的案例。 可以在读取和比较标记的同时从缓存中读取块,因此一旦块地址可用,块读取就开始。 如果读取命中,则块的请求部分会立即传递给处理器。 如果错过,除了台式机和服务器计算机的更多功能之外,没有好处也没有坏处; 只需忽略读取的值。

写操作不允许这种乐观情绪。 在检查标记以查看地址是否命中之前,无法开始修改块。 由于标记检查不能并行进行,因此写入通常比读取花费的时间更长。 另一个复杂性是处理器还指定了写入的大小,通常在 1 到 8 个字节之间; 只能更改块的那部分。 相比之下,读取可以毫无顾虑地访问比必要更多的字节。

写策略通常区分缓存设计。 写入缓存时有两个基本选项:

直写—信息被写入缓存中的块和低级内存中的块
回写——信息仅写入缓存中的块。 修改后的缓存块只有在被替换时才会写入主内存。

为了减少在替换时回写块的频率,通常使用称为脏位的功能。 此状态位指示块是脏的(在缓存中时被修改)还是干净的(未修改)。 如果它是干净的,则该块不会在未命中时写回,因为在较低级别中可以找到与缓存相同的信息。

回写和直写都有其优点。 使用回写,写入以高速缓存存储器的速度发生,并且块内的多次写入只需要向较低级别的存储器写入一次。 由于某些写入不会进入内存,因此回写使用较少的内存带宽,这使得回写在多处理器中具有吸引力。 由于回写使用剩余的内存层次结构和内存互连比直写少,因此它还可以节省功耗,使其对嵌入式应用程序具有吸引力。

直写比回写更容易实现。 缓存始终是干净的,因此与回写读未命中不同,永远不会导致写入到较低级别。 直写还具有下一个较低级别具有数据的最新副本的优点,这简化了数据一致性。 数据一致性对于多处理器和 I/O 很重要,我们在第 4 章和附录 D 中进行了检查。 多级缓存使上层缓存的直写更可行,因为写入只需要传播到下一个较低级别而不是全部 到主存的方式。

正如我们将看到的,I/O 和多处理器是善变的:他们希望为处理器缓存写回以减少内存流量并通过直写来保持缓存与较低级别的内存层次结构保持一致。

当处理器在直写期间必须等待写入完成时,处理器被称为写停顿。 减少写停顿的常见优化是写缓冲区,它允许处理器在数据写入缓冲区后立即继续,从而使处理器执行与内存更新重叠。 正如我们将很快看到的,即使使用写缓冲区也可能发生写停顿。

由于写入时不需要数据,因此写入未命中有两种选择:

写分配——在写未命中时分配块,然后是之前的写命中操作。 在这个自然选项中,写未命中就像读未命中一样。
无写分配——这个明显不寻常的替代方案是写未命中不影响缓存。 相反,该块仅在较低级别的内存中被修改。

因此,在程序尝试读取块之前,块在无写分配中保持在缓存之外,但即使只写入的块仍将在具有写分配的缓存中。 让我们看一个例子。
在这里插入图片描述
写入未命中策略可以与直写或回写一起使用。 通常,回写缓存使用写分配,希望对该块的后续写入将被缓存捕获。 直写缓存通常使用无写分配。 理由是,即使对该块有后续写入,写入仍然必须到较低级别的内存中,那么有什么好处呢?

An Example: The Opteron Data Cache

在这里插入图片描述

图 B.5 显示了 AMD Opteron 微处理器中数据缓存的组织结构,为这些想法提供了实质内容。 高速缓存包含 64 字节块中的 65,536 (64 K) 字节数据,具有双向设置关联放置、最近最少使用的替换、回写和写未命中时的写分配。

让我们按照图 B.5 中标记的命中步骤来跟踪缓存命中。 (四个步骤显示为带圆圈的数字。)如 B.5 节所述,Opteron 向缓存提供 48 位虚拟地址以进行标记比较,同时将其转换为 40 位物理地址。

Opteron不使用所有64位虚拟地址的原因是其设计人员尚不考虑任何人需要这么多的虚拟地址空间,并且较小的尺寸简化了Opteron虚拟地址映射。设计人员计划在未来的微处理器中展现虚拟地址。

进入缓存的物理地址分为两个字段:34 位块地址和 6 位块偏移(64=2^6 和 34 + 6=40)。 块地址进一步分为地址标签和缓存索引。 步骤 1 显示了这种划分。

缓存索引选择要测试的标签以查看所需块是否在缓存中。 索引的大小取决于缓存大小、块大小和集合关联性。对于 Opteron 缓存,集合关联性设置为 2,我们计算索引如下:
在这里插入图片描述
因此,索引为 9 位宽,标签为 34%9 或 25 位宽。 尽管这是选择正确块所需的索引,但 64 字节比处理器想要一次消耗的要多得多。 因此,将高速缓存的数据部分组织为 8 字节宽更有意义,这是 64 位 Opteron 处理器的自然数据字。 因此,除了用于索引正确缓存块的 9 位之外,来自块偏移的另外 3 位用于索引正确的 8 字节。 索引选择是图 B.5 中的步骤 2。

从缓存中读取两个标签后,将它们与来自处理器的块地址的标签部分进行比较。 此比较是图中的第 3 步。 为了确保标签包含有效信息,必须设置有效位,否则比较结果将被忽略。

假设一个标签匹配,最后一步是通过使用来自 2:1 多路复用器的获胜输入,向处理器发出信号以从缓存加载正确的数据。 Opteron 允许这四个步骤有 2 个时钟周期,因此如果接下来的 2 个时钟周期中的指令尝试使用加载结果,它们将等待。

处理写入比处理 Opteron 中的读取更复杂,因为它在任何缓存中。 如果要写入的字在缓存中,前三步是一样的。 因为 Opteron 是乱序执行的,只有在它发出指令已经提交并且缓存标记比较表明命中后,数据才会写入缓存。

到目前为止,我们已经假设了缓存命中的常见情况。 错过了怎么办? 在读取未命中时,缓存向处理器发送一个信号,告诉它数据尚不可用,并从层次结构的下一级读取 64 字节。延迟是块的前 8 个字节的 7 个时钟周期,然后是块的其余部分的每 8 个字节的 2 个时钟周期。 因为数据缓存是关联的,所以可以选择替换哪个块。 Opteron 使用 LRU,它选择引用时间最长的块,因此每次访问都必须更新 LRU 位。 替换块意味着更新数据、地址标签、有效位和 LRU 位。

由于 Opteron 使用回写,旧数据块可能已被修改,因此不能简单地丢弃。 Opteron 为每个块保留 1 个脏位以记录该块是否已写入。 如果“受害者”被修改,它的数据和地址被发送到受害者缓冲区。 (这个结构类似于其他计算机中的写缓冲区。)Opteron 有八个受害者块的空间。 与其他缓存操作并行,它将牺牲块写入层次结构的下一个级别。 如果受害者缓冲区已满,则缓存必须等待。

写未命中与读未命中非常相似,因为 Opteron 会在读或写未命中时分配块。

我们已经看到了它是如何工作的,但是数据缓存不能满足处理器的所有内存需求:处理器还需要指令。 尽管单个缓存可以尝试同时提供两者,但它可能是一个瓶颈。 例如,当执行加载或存储指令时,流水线处理器将同时请求数据字和指令字。 因此,单个缓存会给加载和存储带来结构性危害,导致停顿。 解决这个问题的一种简单方法是将其划分:一个缓存专用于指令,另一个专用于数据。 在最新的处理器中可以找到单独的缓存,包括 Opteron。因此,它具有 64 KiB 指令缓存和 64 KiB 数据缓存。

处理器知道它发出的是指令地址还是数据地址,因此两者可以有单独的端口,从而使存储器层次结构和处理器之间的带宽加倍。 单独的缓存还提供了单独优化每个缓存的机会:不同的容量、块大小和关联性可能会带来更好的性能。 (与 Opteron 的指令缓存和数据缓存相反,术语统一或混合适用于可以包含指令或数据的缓存。)

在这里插入图片描述

图 B.6 显示指令缓存的未命中率低于数据缓存。 分离指令和数据消除了由于指令块和数据块之间的冲突而导致的未命中,但分离也固定了专用于每种类型的缓存空间。 错过率哪个更重要? 单独的指令和数据缓存与统一缓存的公平比较要求总缓存大小相同。 例如,单独的 16 KiB 指令缓存和 16 KiB 数据缓存应该与 32 KiB 统一缓存进行比较。 使用单独的指令和数据缓存计算平均未命中率需要了解每个缓存的内存引用百分比。 从附录 A 中的数据我们发现分裂是100%/(100% + 26% + 10%) 或大约 74% 的指令参考 (26% + 10%)/(100% + 26% + 10%) 或大约 26% 的数据参考。 拆分对性能的影响超出了未命中率变化所指示的范围,我们很快就会看到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值