2 Memory Hierarchy Design

理想情况下,我们希望拥有一个无限大的内存容量,使得任何特定的词汇都能立即获得。然而,我们被迫认识到,必须构建一个内存层次结构,每个层次的内存容量都比前一个层次大,但访问速度较慢。
—— A. W. Burks、H. H. Goldstine 和 J. von Neumann,《电子计算仪逻辑设计的初步讨论》(1946年)。
2.1 Introduction
计算机先驱们正确预测到程序员将会希望拥有无限量的快速内存。对此需求的经济解决方案是内存层次结构,它利用了局部性原则以及内存技术在成本和性能上的权衡。局部性原则(在第一章中介绍)指出,大多数程序不会均匀地访问所有的代码或数据。局部性分为时间局部性(temporal locality)和空间局部性(spatial locality)。这个原则加上这样一个指导方针:对于给定的实现技术和功率预算,较小的硬件可以变得更快,这导致了基于不同速度和大小内存的层次结构。图2.1展示了几种不同的多级内存层次结构,包括典型的访问速度和大小。随着Flash和下一代内存技术在每比特成本上逐渐缩小与磁盘的差距,这些技术很可能会越来越多地取代磁性磁盘作为二级存储。如图2.1所示,这些技术已经在许多个人计算机中使用,并且在服务器中使用越来越多,因为它们在性能、功率和密度方面的优势是显著的。

图2.1 显示了在个人移动设备(PMD)如手机或平板电脑(A)、笔记本电脑或台式计算机(B)、以及服务器(C)中的典型内存层次结构。随着离处理器的距离增加,下一层级的内存变得更慢且更大。需要注意的是,在磁性磁盘的情况下,时间单位从皮秒变化到毫秒,变化因子为10^9,而大小单位从千字节变化到十几TB,变化因子为10^10。如果我们将数据中心级别的计算机(而不仅仅是服务器)加入考虑,容量规模将增加三到六个数量级。闪存组成的固态硬盘(SSD)在个人移动设备中被专门使用,并且在笔记本电脑和台式机中也被广泛使用。在许多台式机中,主要的存储系统是SSD,扩展磁盘主要是硬盘驱动器(HDD)。类似地,许多服务器混合使用SSD和HDD。
由于快速内存更昂贵,因此内存层次结构被组织成几个层级——每一层级比下一层级更小、更快且每字节成本更高,而下一层级则离处理器更远。目标是提供一个内存系统,其每字节成本几乎与最便宜的内存层级一样低,同时其速度也几乎与最快的内存层级一样快。在大多数情况下(但并非所有情况),较低层级中的数据是下一个较高层级的超集。这种特性称为包含性(inclusion property),对于层次结构中的最低层级总是要求具备这种特性,最低层级在缓存的情况下是主内存,在虚拟内存的情况下是二级存储(磁盘或Flash)。
随着处理器性能的提升,内存层次结构的重要性也在增加。图2.2绘制了单处理器性能预测与访问主内存时间的历史性能改进之间的关系。处理器线条显示了每秒内存请求的增加(即内存引用之间延迟的倒数),而内存线条显示了每秒DRAM访问的增加(即DRAM访问延迟的倒数),假设只有一个DRAM和一个内存银行。实际情况更为复杂,因为处理器请求速率并不均匀,内存系统通常有多个DRAM银行和通道。尽管访问时间的差距多年来显著增加,但单处理器性能的改进有限,导致处理器与DRAM之间的差距增长减缓。

图2.2 从1980年的性能基线开始,绘制了处理器内存请求(针对单个处理器或核心)和DRAM访问延迟之间的时间差距的性能差距随时间的变化。到2017年中期,AMD、Intel和Nvidia都宣布使用版本的HBM技术的芯片组。注意,垂直轴必须使用对数刻度来记录处理器-DRAM性能差距的大小。内存基线为1980年的64 KiB DRAM,延迟性能每年提高1.07倍(见第88页的图2.4)。处理器线条假设1986年之前每年提高1.25倍,2000年之前提高1.52倍,2000到2005年之间提高1.20倍,而2005到2015年之间处理器性能(每核心基础上)的提高很小。如图所示,直到2010年,DRAM中的内存访问时间改善缓慢但稳定;自2010年以来,相比早期时期,访问时间的改善有所减少,尽管带宽仍持续提升。有关更多信息,请参见第1章的图1.1。
由于高端处理器具有多个核心,其带宽需求高于单核处理器。尽管单核带宽近年来增长缓慢,但随着核心数量的增加,CPU内存需求与DRAM带宽之间的差距仍在扩大。现代高端桌面处理器如Intel Core i7 6700每个核心每个时钟周期可以生成两个数据内存引用。以4个核心和4.2 GHz的时钟频率为例,i7可以产生高达32.8亿个64位数据内存引用每秒,以及约12.8亿个128位指令引用的峰值指令需求;总峰值需求带宽为409.6 GiB/s!这一惊人的带宽通过缓存的多端口和流水线技术实现;通过使用三个级别的缓存,每个核心有两个私有级别和一个共享的L3;以及在第一级使用独立的指令和数据缓存来实现。相比之下,使用两个内存通道的DRAM主内存的峰值带宽仅为需求带宽的8%(34.1 GiB/s)。预计即将推出的版本将有一个使用嵌入式或堆叠DRAM的L4 DRAM缓存(见第2.2和2.3节)。
传统上,内存层次结构的设计师专注于优化平均内存访问时间,这一时间由缓存访问时间、缺失率和缺失惩罚决定。然而,近年来,功耗成为了一个主要考虑因素。在高端微处理器中,可能会有60 MiB或更多的片上缓存,大型的二级或三级缓存会消耗大量电力,包括在不运行时的漏电流(称为静态功耗)和在执行读写操作时的动态功耗(称为动态功耗),如第2.3节所述。在便携式设备(PMDs)的处理器中,问题更加严重,因为这些处理器的CPU往往不那么积极,且功耗预算可能小20到50倍。在这种情况下,缓存可能占总功耗的25%到50%。因此,更多的设计必须同时考虑性能和功耗的权衡,本章将对这两方面进行讨论。
内存层次结构基础:快速回顾
这一差距的不断扩大及其重要性促使内存层次结构的基础知识进入计算机体系结构的本科课程,甚至扩展到操作系统和编译器课程。因此,我们将从缓存及其操作的快速回顾开始。然而,本章的大部分内容将描述针对处理器—内存性能差距的更先进的创新。
当一个字在缓存中未被找到时,该字必须从层次结构的较低层级(可能是另一个缓存或主内存)中提取,并放置到缓存中,然后才能继续操作。为了效率原因,通常会移动多个字,这些字被称为块(或行),因为它们由于空间局部性很可能很快被需要。每个缓存块包括一个标签,用于指示其对应的内存地址。
一个关键的设计决策是块(或行)可以放置在缓存中的位置。最常见的方案是集合关联,其中一个集合是一组缓存块。一个块首先被映射到一个集合中,然后可以放置在该集合中的任何位置。找到一个块的过程包括首先将块地址映射到集合中,然后在集合中—通常是并行—搜索该块。集合是通过数据的地址选择的。

如果一个集合中有 \( n \) 个块,那么缓存的放置方式称为 \( n \)-路集合关联(\( n \)-way set associative)。集合关联的两个极端有各自的名称。直接映射缓存(direct-mapped cache)每个集合只有一个块(因此,一个块总是放置在相同的位置),而全关联缓存(fully associative cache)只有一个集合(因此,一个块可以放置在任何位置)。
缓存只读数据比较简单,因为缓存中的副本和内存中的副本将是相同的。缓存写操作则更复杂;例如,如何保持缓存和内存中的副本一致?主要有两种策略。
1. **写透缓存(write-through cache)**:更新缓存中的项并同时更新主内存。
2. **写回缓存(write-back cache)**:仅更新缓存中的副本。当块即将被替换时,将其复制回内存。两种写策略都可以使用写缓冲区(write buffer),允许缓存只要数据被放置在缓冲区中就继续操作,而不必等待完全延迟写入内存。
不同缓存组织的效益可以通过未命中率(miss rate)来衡量。未命中率是缓存访问中导致未命中的比例,即未命中的访问次数除以总访问次数。
为了深入了解高未命中率的原因,这有助于激发更好的缓存设计,三类模型(three Cs model)将所有未命中分为三类:
- **强制性未命中(Compulsory)**:对块的第一次访问不可能在缓存中,因此该块必须被带入缓存。强制性未命中是指即使缓存无限大也会发生的未命中。
- **容量未命中(Capacity)**:如果缓存无法容纳执行程序所需的所有块,将发生容量未命中(除了强制性未命中),因为块被丢弃后又被重新取回。
- **冲突未命中(Conflict)**:如果块放置策略不是全关联的,将发生冲突未命中(除了强制性和容量未命中),因为多个块可能映射到同一个集合,并且对不同块的访问是交织在一起的。
图B.8(第24页)展示了缓存未命中的相对频率,按照三种未命中类型(三C)进行分类。正如附录B中提到的,三C模型是概念性的,尽管其见解通常适用,但它并不是解释单个引用缓存行为的决定性模型。
正如我们在第3章和第5章中将看到的,多线程和多核增加了缓存的复杂性,不仅增加了容量未命中的潜力,还增加了第四种未命中类型,即由于缓存刷新保持多处理器中多个缓存一致性而产生的一致性未命中;我们将在第5章中讨论这些问题。
然而,未命中率可能因多种原因而具有误导性。因此,一些设计师更倾向于测量每条指令的未命中次数,而不是每个内存引用的未命中率。这两者是相关的:

(这个公式通常以整数形式表示,而不是分数形式,例如每1000条指令的未命中次数。)
这两种度量方法的问题在于它们没有考虑未命中的成本。一个更好的度量是平均内存访问时间。

其中,命中时间是指在缓存中命中的时间,而未命中惩罚是指从内存中替换块的时间(即未命中的成本)。平均内存访问时间仍然是间接的性能度量;虽然它比未命中率更好,但不能替代执行时间。在第3章中,我们将看到投机处理器可能在未命中期间执行其他指令,从而减少有效未命中惩罚。多线程(在第3章介绍)也允许处理器在未命中时继续工作,而不必闲置。正如我们将很快讨论的,为了利用这种延迟容忍技术,我们需要能够在处理未命中请求时继续服务的缓存。
如果这些内容对你来说较新,或者这个快速回顾过于简略,请参阅附录B。附录B对这些基础内容进行了更深入的讲解,并包括了真实计算机的缓存示例和其有效性的定量评估。
附录B的第B.3节介绍了六种基本缓存优化,我们在这里快速回顾了这些内容。附录还提供了这些优化的定量效果示例,并简要评论了这些权衡的功耗影响。
1. **增大块大小以减少未命中率**—最简单的方法是利用空间局部性,增加块大小。较大的块可以减少强制未命中,但也会增加未命中惩罚。由于较大的块降低了标签的数量,它们可以略微减少静态功耗。较大的块还可能增加容量或冲突未命中,特别是在较小的缓存中。选择合适的块大小是一个复杂的权衡,取决于缓存的大小和未命中惩罚。
2. **增加缓存大小以减少未命中率**—减少容量未命中的显而易见的方法是增加缓存容量。缺点包括可能更长的缓存命中时间以及更高的成本和功耗。较大的缓存会增加静态和动态功耗。
3. **提高关联度以减少未命中率**—显然,提高关联度可以减少冲突未命中。但更高的关联度可能会增加命中时间。正如我们将看到的,关联度也会增加功耗。
4. **多级缓存以减少未命中惩罚**—一个困难的决策是选择使缓存命中时间快,以跟上处理器的高时钟频率,还是使缓存更大,以缩小处理器访问和主存访问之间的差距。在原始缓存和内存之间添加另一层缓存可以简化决策。一级缓存可以足够小以匹配快速的时钟周期时间,而二级(或三级)缓存可以足够大以捕捉许多会去主存的访问。二级缓存中的未命中关注导致更大的块、更大的容量和更高的关联度。多级缓存比单一的聚合缓存更节能。如果L1和L2分别指的是一级和二级缓存,我们可以重新定义平均内存访问时间:

5. **优先处理读取未命中而非写入未命中以减少未命中惩罚**—写入缓冲区是实现这一优化的好地方。写入缓冲区可能会造成危险,因为它们保存了读取未命中时需要的内存位置的更新值,即写后读(read-after-write)危险。一种解决方案是,在读取未命中时检查写入缓冲区的内容。如果没有冲突,并且内存系统可用,则在写操作之前发送读取操作可以减少未命中惩罚。大多数处理器优先考虑读取而非写入。这个选择对功耗的影响很小。
6. **避免在缓存索引时进行地址转换以减少命中时间**—缓存必须处理从处理器到物理地址的虚拟地址转换以访问内存。(虚拟内存在第2.4节和附录B.4中讨论。)一种常见的优化是使用页面偏移量——即在虚拟地址和物理地址中相同的部分——来索引缓存,如附录B第B.38页所述。这种虚拟索引/物理标签方法引入了一些系统复杂性和/或对L1缓存的大小和结构的限制,但去除翻译后备缓冲区(TLB)访问的优势大于其劣势。
需要注意的是,上述六种优化中的每一种都有可能的缺点,可能导致平均内存访问时间的增加,而非减少。本章其余部分假定读者熟悉前述材料和附录B中的细节。在“综合分析”部分,我们将考察为高端桌面计算机或较小服务器设计的微处理器内存层次结构,例如Intel Core i7 6700,以及为便携设备(PMD)设计的处理器,例如Arm Cortex-A53,这是几款平板电脑和智能手机中使用的处理器的基础。在这些类别中,由于计算机的预期用途,方法存在显著的多样性。
尽管i7 6700相比于为移动用途设计的Intel处理器具有更多的核心和更大的缓存,但这些处理器具有相似的架构。为小型服务器(如i7 6700)或大型服务器(如Intel Xeon处理器)设计的处理器,通常运行大量的并发进程,通常是不同用户的进程。因此,内存带宽变得更加重要,这些处理器提供更大的缓存和更积极的内存系统以提升带宽。
相比之下,便携设备不仅服务于单一用户,而且通常操作系统较小,通常多任务处理较少(同时运行多个应用程序),且应用程序更简单。便携设备必须考虑性能和能耗,这决定了电池寿命。在深入探讨更高级的缓存组织和优化之前,需要了解各种内存技术及其发展趋势。
2.2 Memory Technology and Optimizations
“使计算机站稳脚跟的唯一一项发展就是发明了一种可靠的存储形式,即磁心存储器。它的成本合理,可靠,而且由于它的可靠性,最终可以制造得很大。”(第209页)
——莫里斯·威尔克斯,《计算机先驱回忆录》(1985年)
这一节描述了内存层次结构中使用的技术,特别是在构建缓存和主内存时使用的技术。这些技术包括 SRAM(静态随机访问内存)、DRAM(动态随机访问内存)和 Flash。最后一种技术作为硬盘的替代方案,但由于其特性基于半导体技术,因此适合在这一节中介绍。
使用 SRAM 可以满足最小化缓存访问时间的需求。然而,当发生缓存未命中时,我们需要尽可能快速地从主内存中移动数据,这就需要高带宽的内存。这种高带宽内存可以通过将构成主内存的多个 DRAM 芯片组织成多个内存银行,并将内存总线加宽,或者同时进行这两种方式来实现。
为了使内存系统能够跟上现代处理器的带宽需求,内存创新开始在 DRAM 芯片内部发生。这一节将描述内存芯片内部的技术及其创新的内部组织。在描述技术和选项之前,我们需要介绍一些术语。
随着突发传输内存的引入,现在广泛用于 Flash 和 DRAM,内存延迟使用两个指标来表示——访问时间和周期时间。访问时间是从发起读取请求到期望的数据到达之间的时间,而周期时间是无关请求之间的最小时间。
几乎所有计算机从1975年起都使用 DRAM 作为主内存,SRAM 作为缓存,且通常将一至三级集成在处理器芯片上。便携设备必须在功耗和性能之间取得平衡,由于其存储需求较小,便携设备使用 Flash 而非硬盘驱动器,这一决策也越来越被桌面计算机所采纳。
**SRAM 技术**
SRAM 的第一个字母代表“静态”。DRAM 中电路的动态特性要求在读取数据后必须重新写入,因此访问时间和周期时间之间存在差异,并且需要刷新。而 SRAM 不需要刷新,因此其访问时间非常接近周期时间。SRAM 通常使用六个晶体管来存储每一位数据,以防在读取时信息被干扰。SRAM 仅需极少的电力即可在待机模式下保持电荷。
在早期,大多数桌面计算机和服务器系统使用 SRAM 芯片作为其主缓存、二级缓存或三级缓存。今天,这三级缓存通常集成在处理器芯片上。在高端服务器芯片中,可能有多达 24 个核心和多达 60 MiB 的缓存;这样的系统通常配置有每个处理器芯片 128–256 GiB 的 DRAM。大型三级缓存的访问时间通常是二级缓存的两到八倍。即便如此,L3 缓存的访问时间通常至少比 DRAM 访问时间快五倍。
芯片上的缓存 SRAM 通常以与缓存块大小匹配的宽度组织,标签并行存储于每个块。这允许在一个周期内读取或写入整个块。这种能力在写入因未命中而获取的数据到缓存中或写回必须从缓存中驱逐的块时特别有用。缓存的访问时间(忽略集合关联缓存中的命中检测和选择)与缓存中的块数量成正比,而能耗则取决于缓存中的位数(静态功耗)和块的数量(动态功耗)。集合关联缓存减少了对内存的初始访问时间,因为内存的大小较小,但增加了命中检测和块选择的时间,这一话题我们将在第 2.3 节中讨论。
**DRAM 技术**
随着早期 DRAM 的容量增加,包含所有必要地址线的封装成本成为问题。解决方案是对地址线进行复用,从而将地址引脚的数量减半。图 2.3 显示了基本的 DRAM 组织结构。在行访问时钟(RAS)期间,首先发送地址的一半。另一半地址在列访问时钟(CAS)期间发送。这些名称源于内部芯片的组织,因为内存按行和列组织成一个矩形矩阵。

**图 2.3 DRAM 的内部组织结构**
现代 DRAM 组织为多个银行,DDR4 支持最多 16 个银行。每个银行由一系列行组成。发送 ACT(激活)命令会打开一个银行和一行,并将该行加载到行缓冲区。当行在缓冲区中时,可以通过连续的列地址进行传输,具体传输宽度取决于 DRAM 的宽度(在 DDR4 中通常为 4、8 或 16 位),或者通过指定块传输及起始地址进行传输。预充电命令(PRE)关闭银行和行,并为新的访问做好准备。每个命令以及块传输都与时钟同步。请参阅下一节讨论 SDRAM。行和列信号有时被称为 RAS 和 CAS,基于这些信号的原始名称。
DRAM 的一个额外要求来源于其首字母 D 代表的动态特性。为了在每个芯片上存储更多位,DRAM 只使用一个晶体管,实际上充当电容器来存储一个比特。这有两个影响:首先,检测电荷的感应线必须预充电,使它们处于逻辑 0 和 1 之间的“中间”状态,允许存储在单元中的微小电荷被感应放大器检测为 0 或 1。在读取时,一行数据被放入行缓冲区,CAS 信号可以选择该行的一部分从 DRAM 中读取出来。由于读取一行会销毁信息,因此在该行不再需要时必须将其写回。这个写回是重叠进行的,但在早期 DRAM 中,这意味着在可以读取新行之前的周期时间大于读取一行并访问该行一部分的时间。
此外,为了防止信息丢失,因为单元中的电荷会泄漏(假设没有被读取或写入),每个位必须定期“刷新”。幸运的是,只需读取该行并将其写回,即可同时刷新行中的所有位。因此,内存系统中的每个 DRAM 必须在一定时间窗口内访问每一行,比如 64 毫秒。DRAM 控制器包括硬件来定期刷新 DRAM。
这一要求意味着内存系统偶尔会不可用,因为它需要向每个芯片发送信号进行刷新。刷新所需的时间包括行激活和预充电,并将行数据写回(这大约需要总时间的 2/3,因为不需要列选择),每行 DRAM 都需要进行这样的操作。由于 DRAM 的内存矩阵在概念上是正方形的,刷新所需的步骤数通常是 DRAM 容量的平方根。DRAM 设计者试图将刷新时间保持在总时间的 5% 以下。到目前为止,我们介绍的主要内存仿佛像瑞士列车一样,始终按照计划精确交付。实际上,对于 SDRAM,DRAM 控制器(通常在处理器芯片上)尝试通过避免打开新行和在可能的情况下使用块传输来优化访问。刷新增加了另一个不可预测的因素。
Amdahl 建议作为经验法则,内存容量应与处理器速度线性增长,以保持系统平衡。因此,一个 1000 MIPS 的处理器应该有 1000 MiB 的内存。处理器设计师依赖 DRAM 来满足这种需求。过去,他们期望每三年容量提高四倍,或者每年提高 55%。不幸的是,DRAM 的性能增长速度远远较慢。这种性能增长缓慢主要由于行访问时间的减少较小,而行访问时间受限于功率限制和单个内存单元的电荷容量(以及大小)。在我们更详细地讨论这些性能趋势之前,需要描述从 1990 年代中期开始 DRAM 发生的重大变化。
提升 DRAM 芯片内部内存性能:SDRAMs
虽然早期的 DRAM 包括一个缓冲区,允许对单行进行多个列访问而不需要新的行访问,但它们使用了异步接口,这意味着每个列访问和传输都涉及到与控制器同步的开销。在 1990 年代中期,设计师向 DRAM 接口添加了时钟信号,从而消除了重复传输的开销,创建了同步 DRAM(SDRAM)。除了减少开销,SDRAM 还允许添加突发传输模式,在这种模式下,多个传输可以在不指定新列地址的情况下进行。通常,通过将 DRAM 设置为突发模式,可以进行八次或更多次 16 位传输而无需发送任何新地址。突发模式传输的引入意味着在随机访问流和数据块访问之间存在显著的带宽差距。
为了在 DRAM 密度增加时获得更多带宽,DRAM 被设计得更宽。最初,它们提供了四位传输模式;到 2017 年,DDR2、DDR3 和 DDR DRAM 的总线宽度达到了 4、8 或 16 位。
在 2000 年代初,进一步的创新被引入:双倍数据速率(DDR),它允许 DRAM 在内存时钟的上升沿和下降沿进行数据传输,从而使峰值数据速率翻倍。
最后,SDRAM 引入了多个存储银行,以帮助管理功耗、提高访问时间,并允许对不同银行进行交错和重叠的访问。对不同银行的访问可以相互重叠,每个银行都有自己的行缓冲区。在 DRAM 内部创建多个银行实际上是在地址中添加了另一个分段,现在的地址包括银行号、行地址和列地址。当发送一个指定新银行的地址时,该银行必须被打开,产生额外的延迟。现代内存控制接口完全处理银行和行缓冲区的管理,因此当后续访问指定了已打开银行的相同行时,可以快速访问,只需发送列地址即可。
要启动新的访问,DRAM 控制器发送一个银行和行号(在 SDRAM 中称为 Activate,之前称为 RAS——行选择)。该命令打开行并将整行读入缓冲区。然后可以发送列地址,SDRAM 可以传输一个或多个数据项,具体取决于是单项请求还是突发请求。在访问新行之前,银行必须预充电。如果行在同一银行中,则会经历预充电延迟;然而,如果行在其他银行中,则关闭行和预充电可以与访问新行重叠。在同步 DRAM 中,每个命令周期需要一个完整的时钟周期。
从 1980 年到 1995 年,DRAM 的容量随着摩尔定律的推进,每 18 个月翻倍(或每 3 年翻 4 倍)。从 1990 年代中期到 2010 年,容量增加的速度较慢,大约每 26 个月翻倍。从 2010 年到 2016 年,容量仅翻倍!图 2.4 显示了不同代 DDR SDRAM 的容量和访问时间。从 DDR1 到 DDR3,访问时间提高了大约 3 倍,或每年约 7%。DDR4 相比 DDR3 提升了功耗和带宽,但访问延迟相似。
如图 2.4 所示,DDR 是一系列标准的延续。DDR2 通过将电压从 2.5 V 降低到 1.8 V 来减少功耗,并提供更高的时钟频率:266、333 和 400 MHz。DDR3 将电压降低到 1.5 V,最大时钟速度为 800 MHz。(正如我们在下一节中讨论的,GDDR5 是一种图形内存,基于 DDR3 DRAM。)DDR4 于 2016 年初大规模上市,但原定于 2014 年发布,将电压降低到 1–1.2 V,最大预期时钟频率为 1600 MHz。DDR5 可能要到 2020 年或更晚才能达到生产数量。

图 2.4 显示了按生产年份划分的 DDR SDRAM 的容量和访问时间。访问时间指的是对一个随机内存字的访问,并假设需要打开一个新行。如果该行位于不同的银行,我们假设该银行已预充电;如果行尚未打开,则需要进行预充电,此时访问时间会更长。随着银行数量的增加,隐藏预充电时间的能力也有所提高。DDR4 SDRAM 最初预计在 2014 年推出,但直到 2016 年初才开始生产。
随着 DDR 的引入,内存设计师越来越关注带宽,因为提升访问时间变得困难。更宽的 DRAM、突发传输和双倍数据速率都促进了内存带宽的快速增长。DRAM 通常以称为双列直插内存模块(DIMM)的较小板卡出售,这些板卡包含 4 到 16 个 DRAM 芯片,通常组织为 8 字节宽(+ ECC)用于桌面和服务器系统。当 DDR SDRAM 被封装成 DIMM 时,它们通常根据峰值 DIMM 带宽进行标记,因此 DIMM 名称 PC3200 来自于 200 MHz × 2 × 8 字节,即 3200 MiB/s;它装配有 DDR SDRAM 芯片。为了增加混淆,芯片本身标记的是每秒位数而不是时钟频率,因此一个 200 MHz 的 DDR 芯片被称为 DDR400。图 2.5 显示了 I/O 时钟频率、每芯片每秒传输次数、芯片带宽、芯片名称、DIMM 带宽和 DIMM 名称之间的关系。

图 2.5 显示了 2016 年 DDR DRAM 和 DIMM 的时钟频率、带宽和名称。请注意各列之间的数字关系。第三列的数值是第二列的两倍,第四列的名称中使用了第三列的数字。第五列的数值是第三列的八倍,并且这个数字的四舍五入值被用于 DIMM 的名称。DDR4 在 2016 年首次得到了显著应用。
降低 SDRAM 功耗
动态内存芯片的功耗包括读写过程中使用的动态功耗和静态或待机功耗;这两者都依赖于操作电压。在最先进的 DDR4 SDRAM 中,操作电压已降低至 1.2 V,相比于 DDR2 和 DDR3 SDRAM,功耗显著减少。由于只有单个银行中的行被读取,增加银行数量也减少了功耗。
除了这些变化,所有近期的 SDRAM 都支持一个省电模式,该模式通过让 DRAM 忽略时钟来进入。省电模式会禁用 SDRAM,除了内部的自动刷新(如果没有刷新,长时间进入省电模式会导致内存内容丢失)。图 2.6 显示了 2 GB DDR3 SDRAM 在三种情况下的功耗。返回低功耗模式所需的具体延迟取决于 SDRAM,但典型的延迟是 200 个 SDRAM 时钟周期。

图 2.6 显示了 DDR3 SDRAM 在三种条件下的功耗:低功耗(关闭)模式、典型系统模式(DRAM 在读操作中活跃 30% 的时间,在写操作中活跃 15% 的时间),以及完全活跃模式,其中 DRAM 持续进行读取或写入操作。读取和写入操作假定为八次传输的突发。这些数据基于 Micron 1.5V 2GB DDR3-1066,类似的节能效果也适用于 DDR4 SDRAM。
图形数据 RAM(GDRAMs 或 GSDRAMs,图形或图形同步 DRAMs)是一类特殊的 DRAM,基于 SDRAM 设计,但经过调整以处理图形处理单元对带宽的更高需求。GDDR5 基于 DDR3,而早期的 GDDR 则基于 DDR2。由于图形处理单元(GPU;见第 4 章)每个 DRAM 芯片需要比 CPU 更高的带宽,GDDR 有几个重要的不同点:
1. GDDR 的接口更宽:32 位,而当前设计中的 DRAM 通常为 4、8 或 16 位。
2. GDDR 在数据引脚上的最高时钟频率更高。为了允许更高的传输速率而不产生信号问题,GDDR 通常直接连接到 GPU,并通过焊接方式附着在板上,这与通常以可扩展的 DIMM 阵列形式排列的 DRAM 不同。
总的来说,这些特性使得 GDDR 的带宽是 DDR3 DRAM 的两到五倍。
**封装创新:堆叠或嵌入式 DRAM**
2017 年 DRAM 的最新创新是封装创新,而非电路创新。这种创新将多个 DRAM 以堆叠或相邻的方式嵌入到与处理器相同的封装中。(嵌入式 DRAM 也用于指将 DRAM 放置在处理器芯片上的设计。)将 DRAM 和处理器放置在同一封装中可以降低访问延迟(通过缩短 DRAM 和处理器之间的延迟)并潜在地增加带宽,因为这允许处理器和 DRAM 之间有更多更快的连接;因此,许多生产商称之为高带宽内存(HBM)。
这种技术的一个版本是将 DRAM 芯片直接放置在 CPU 芯片上,并使用焊球技术连接它们。假设有足够的散热管理,多个 DRAM 芯片可以以这种方式堆叠。另一种方法则是仅堆叠 DRAM,并将其与 CPU 使用一个包含连接的基板(中介层)放在一个封装中。图 2.7 展示了这两种不同的互连方案。已经展示了允许堆叠多达八个芯片的 HBM 原型。使用特殊版本的 SDRAM,这种封装可以容纳 8 GiB 的内存,并具有 1 TB/s 的数据传输速率。2.5D 技术目前已可用。由于芯片必须专门制造以适应堆叠,因此最早的应用可能主要集中在高端服务器芯片组中。

在某些应用中,可能可以内部封装足够的 DRAM 以满足应用需求。例如,正在开发一种用于特殊目的集群设计的 Nvidia GPU 版本,该版本使用 HBM,并且 HBM 可能会成为高端应用的 GDDR5 的继任者。在某些情况下,可能可以将 HBM 用作主内存,尽管成本限制和散热问题目前使这种技术不适用于某些嵌入式应用。在下一部分中,我们将考虑将 HBM 用作额外缓存层的可能性。
**闪存**
闪存是一种 EEPROM(电子可擦写可编程只读存储器),通常是只读的,但可以被擦除。闪存的另一个关键特性是它能够在没有电力的情况下保持其内容。我们重点关注 NAND 闪存,因为它的密度比 NOR 闪存更高,更适合大规模非易失性存储器;缺点是访问是顺序的,写入速度较慢,具体如下。
闪存作为 PMD(便携式移动设备)中的二级存储器,其功能类似于笔记本电脑或服务器中的磁盘。此外,由于大多数 PMD 的 DRAM 数量有限,闪存还可能作为内存层级的一部分,比桌面或服务器中 10-100 倍大的主内存要大得多。
闪存使用与标准 DRAM 完全不同的架构,具有不同的特性。主要区别包括:
1. 读取闪存是顺序的,并且读取整个页面,这可以是 512 字节、2 KiB 或 4 KiB。因此,NAND 闪存从随机地址访问第一个字节的延迟较长(大约 25 μs),但可以以大约 40 MiB/s 的速度提供页面块的其余部分。相比之下,DDR4 SDRAM 获取第一个字节需要大约 40 ns,且可以以 4.8 GiB/s 的速度传输剩余的行。比较传输 2 KiB 的时间,NAND 闪存大约需要 75 μs,而 DDR SDRAM 少于 500 ns,使得闪存速度约为 150 倍慢。然而,与磁盘相比,从闪存读取 2 KiB 的速度快 300 到 500 倍。由此可见,闪存不适合替代 DRAM 作为主内存,但有潜力替代磁盘。
2. 闪存必须在被重写之前擦除(因此有“闪光”擦除过程的名称),并且是以块的形式擦除,而不是单独的字节或字。这一要求意味着在向闪存写入数据时,必须将整个块组装起来,或者作为新数据,或者通过合并待写数据和块的其余内容来完成。对于写入操作,闪存速度大约是 SDRAM 的 1500 倍慢,而比磁盘快 8-15 倍。
3. 闪存是非易失性的(即在没有电力时也能保持内容),并且在不读取或写入时消耗的电力显著减少(在待机模式下为原来的一半以下,在完全不活动时为零)。
4. 闪存限制了任何给定块的写入次数,通常至少为 100,000 次。通过确保写入块在内存中的均匀分布,系统可以最大化闪存系统的寿命。这种技术称为写入均衡,由闪存控制器处理。
5. 高密度 NAND 闪存比 SDRAM 便宜,但比磁盘更贵:闪存约为 $2/GiB,SDRAM 约为 $20 至 $40/GiB,磁盘约为 $0.09/GiB。在过去五年中,闪存的成本下降速度几乎是磁盘的两倍。
像 DRAM 一样,闪存芯片包含冗余块,以允许存在少量缺陷的芯片得以使用;块的重新映射在闪存芯片中处理。闪存控制器负责页面传输、页面缓存以及写入均衡。
高密度闪存的快速进步对低功耗便携设备和笔记本电脑的发展至关重要,但也显著改变了桌面计算机(它们越来越多地使用固态硬盘)以及大型服务器(通常结合了磁盘和闪存存储)。
**相变存储器技术**
相变存储器(PCM)已成为一个活跃的研究领域几十年。该技术通常使用一个小的加热元件来改变基质的状态,使其在晶态和非晶态之间转换,这两种状态具有不同的电阻特性。每个位对应于覆盖基质的二维网络中的一个交点。读取操作是通过感测 x 点和 y 点之间的电阻来完成的(因此有了“忆阻器”这一替代名称),而写入操作则是通过施加电流来改变材料的相态。由于没有主动设备(如晶体管),因此其成本应低于 NAND 闪存,密度也更高。
2017 年,Micron 和 Intel 开始交付被认为基于 PCM 的 Xpoint 存储芯片。预计该技术的写入耐久性比 NAND 闪存要好得多,并且通过消除在写入前擦除页面的需求,写入性能有望比 NAND 提高多达十倍。读取延迟也可能比闪存好,提升因素大约为 2-3 倍。最初,价格预计会稍高于闪存,但在写入性能和写入耐久性方面的优势可能使其在 SSD 中具有吸引力。如果这种技术能够良好地扩展并实现进一步的成本降低,它可能成为取代磁盘的固态技术,而磁盘作为主要的非易失性大容量存储器已经统治了超过 50 年。
**提升内存系统的可靠性**
大型缓存和主存显著增加了在制造过程中以及操作过程中发生错误的可能性。由电路变化引起的可重复错误称为硬错误或永久性故障。硬错误可以在制造过程中发生,也可以在操作过程中由于电路变化而发生(例如,Flash 存储单元在多次写入后出现故障)。所有 DRAM、Flash 存储和大多数 SRAM 都配备了备用行,以便通过编程将有缺陷的行替换为备用行,从而容纳少量制造缺陷。动态错误,即对单元内容的变化而非电路变化,称为软错误或瞬态故障。
动态错误可以通过奇偶校验位进行检测,通过使用纠错码(ECC)进行检测和修复。由于指令缓存是只读的,奇偶校验就足够了。在较大的数据缓存和主存中,ECC 被用来检测和修复错误。奇偶校验只需要一个附加位来检测一个位序列中的单个错误。由于多位错误将无法被奇偶校验检测到,因此奇偶校验位保护的位数必须有限。每 8 位数据使用一个奇偶校验位是典型的比例。ECC 能够检测两个错误并修复一个错误,其开销为每 64 位数据 8 位的附加位。
在非常大的系统中,多个错误以及单个内存芯片的完全失败的可能性变得显著。IBM 引入了 Chipkill 来解决这个问题,许多大型系统,如 IBM 和 SUN 服务器以及 Google 集群,都使用了这一技术。(英特尔称其版本为 SDDC。)Chipkill 的性质类似于用于磁盘的 RAID 方法,它分配数据和 ECC 信息,使得单个内存芯片的完全失败可以通过支持从剩余内存芯片中重建丢失的数据来处理。根据 IBM 的分析,假设一个拥有每个处理器 4 GiB 的 10,000 处理器服务器,在三年的操作中,以下是无法恢复错误的发生率:
- 仅奇偶校验:约 90,000 次,即每 17 分钟发生一次无法恢复(或未检测到)的故障。
- 仅 ECC:约 3,500 次,即每 7.5 小时发生一次未检测到或无法恢复的故障。
- Chipkill:约每 2 个月发生一次未检测到或无法恢复的故障。
另一种考虑方式是找到可以保护的最大服务器数量(每台服务器 4 GiB),同时实现与 Chipkill 演示的相同错误率。对于奇偶校验,即使是只有一个处理器的服务器,其无法恢复的错误率也高于 10,000 台服务器 Chipkill 保护系统。对于 ECC,17 台服务器系统的故障率与 10,000 台服务器 Chipkill 系统大致相同。因此,Chipkill 是仓库规模计算机(参见第 6 章第 6.8 节)中 50,000 至 100,000 台服务器的必备条件。
2.3 Ten Advanced Optimizations of Cache Performance
之前的平均内存访问时间公式为缓存优化提供了三个度量指标:命中时间、未命中率和未命中惩罚。鉴于近期趋势,我们将缓存带宽和功耗也加入了这份清单。我们可以将我们检查的10种高级缓存优化按这些指标分为五类:
1. **减少命中时间**——小型简单的一级缓存和路径预测。这些技术通常也会减少功耗。
2. **增加缓存带宽**——流水线缓存、多银行缓存和非阻塞缓存。这些技术对功耗的影响各异。
3. **减少未命中惩罚**——关键字优先和合并写缓冲区。这些优化对功耗的影响较小。
4. **减少未命中率**——编译器优化。显然,编译时间的任何改进都会改善功耗。
5. **通过并行性减少未命中惩罚或未命中率**——硬件预取和编译器预取。这些优化通常会增加功耗,主要是因为预取的数据未被使用。
总体而言,随着优化的进行,硬件复杂性会增加。此外,一些优化需要复杂的编译器技术,而最后一种依赖于HBM。我们将总结这10种技术的实施复杂性和性能收益,详见第113页的图2.18。由于其中一些技术比较简单,我们会简要介绍;其他的则需要更多描述。
**首次优化:减少命中时间和功耗的小型简单一级缓存**
在高速时钟周期和功耗限制的压力下,一级缓存的大小被限制在一个相对较小的范围内。同样,较低的关联度水平也可以减少命中时间和功耗,尽管这种权衡比涉及缓存大小的权衡要复杂得多。
在缓存命中的关键时间路径是一个三步过程:使用地址的索引部分访问标记内存,将读取的标记值与地址进行比较,以及在缓存为集合关联时设置多路复用器以选择正确的数据项。直接映射缓存可以将标记检查与数据传输重叠,从而有效减少命中时间。此外,较低的关联度水平通常会减少功耗,因为需要访问的缓存行更少。
尽管新一代微处理器的片上缓存总量大幅增加,但由于较大L1缓存对时钟频率的影响,L1缓存的大小最近有所增加,但增加幅度较小或几乎没有。在许多近期的处理器中,设计师更倾向于增加关联度,而不是增大缓存大小。选择关联度的另一个考虑因素是消除地址别名的可能性;我们将在稍后讨论这一主题。
确定对命中时间和功耗的影响的一个方法是使用CAD工具。CACTI是一个用于估算CMOS微处理器上各种缓存结构的访问时间和能耗的程序,其估算结果在10%以内,接近于更详细的CAD工具。对于给定的最小特征尺寸,CACTI根据缓存大小、关联度、读/写端口数量以及其他更复杂的参数估算缓存的命中时间。
图2.8显示了缓存大小和关联度变化对命中时间的估算影响。根据这些参数的缓存大小模型,直接映射缓存的命中时间略快于双向集合关联缓存,双向集合关联缓存的命中时间是四向缓存的1.2倍,四向缓存的命中时间是八向缓存的1.4倍。当然,这些估算结果取决于技术和缓存大小,CACTI必须与技术保持仔细对齐;图2.8显示了某一技术的相对权衡。

**图2.8**:随着缓存大小和关联度的增加,相对访问时间通常会增加。这些数据来自Tarjan等人(2005年)的CACTI模型6.5。数据假设了典型的嵌入式SRAM技术、单一银行和64字节块。关于缓存布局和复杂的权衡(这些权衡涉及到互连延迟,这取决于被访问的缓存块的大小,以及标记检查和多路复用的成本)导致了有时令人惊讶的结果,例如64 KiB的二路集合关联缓存的访问时间低于直接映射缓存。同样,八路集合关联缓存的结果在缓存大小增加时也表现出异常行为。由于这些观察结果高度依赖于技术和详细的设计假设,因此像CACTI这样的工具有助于减少搜索空间。这些结果是相对的;然而,随着我们进入更新和更密集的半导体技术,这些结果可能会发生变化。
**例题**:使用附录B中图B.8和图2.8的数据,确定32 KiB四路集合关联L1缓存是否比32 KiB两路集合关联L1缓存具有更快的内存访问时间。假设L2的缺失惩罚是较快L1缓存访问时间的15倍。忽略L2之后的缺失。哪一个具有更快的平均内存访问时间?
**答案**:设两路集合关联缓存的访问时间为1。那么,对于两路缓存,

对于四路集合关联缓存,访问时间是1.4倍长。缺失惩罚的经过时间为15/1.4≈10.1。为了简化,假设为10:

显然,更高的关联度看起来是一个不好的权衡;然而,由于现代处理器中的缓存访问通常是流水线化的,因此对时钟周期时间的确切影响很难评估。
能源消耗在选择缓存大小和关联度时也是一个重要的考虑因素,如图2.9所示。在128 KiB或256 KiB的缓存中,从直接映射到两路集合关联的情况下,更高关联度的能源成本范围从超过2倍到微不足道。

图2.9显示了每次读取的能量消耗随着缓存大小和关联度的增加而增加。与之前的图一样,使用CACTI进行建模,采用相同的技术参数。八路集合关联缓存的较大惩罚是由于并行读取八个标签和相应数据的成本。
由于能源消耗变得至关重要,设计师们专注于减少缓存访问所需的能量。除了关联度,决定缓存访问中使用的能量的另一个关键因素是缓存中的块数量,因为它决定了被访问的“行”的数量。设计师可以通过增加块大小(保持总缓存大小不变)来减少行数,但这可能会增加缺失率,尤其是在较小的L1缓存中。
一种替代方案是将缓存组织为多个银行,使得一次访问只激活缓存的一部分,即存放所需块的银行。多银行缓存的主要用途是增加缓存带宽,这是我们稍后会讨论的优化。多银行设计还可以减少能量消耗,因为访问的缓存部分更少。许多多核处理器中的L3缓存在逻辑上是统一的,但在物理上是分布式的,实际上充当了一个多银行缓存。根据请求的地址,实际上只访问一个物理L3缓存(一个银行)。我们将在第5章进一步讨论这种组织方式。
在最近的设计中,有三个其他因素导致尽管有能源和访问时间成本,但仍使用更高的关联度在一级缓存中。首先,许多处理器访问缓存至少需要2个时钟周期,因此较长的命中时间可能不会是关键问题。其次,为了将TLB从关键路径中排除(这种延迟会大于与增加关联度相关的延迟),几乎所有L1缓存都应进行虚拟索引。这将缓存的大小限制为页面大小乘以关联度,因为此时仅使用页面内的位来进行索引。尽管在地址转换完成之前还有其他解决缓存索引问题的方法,但增加关联度,且具有其他好处,是最具吸引力的。第三,随着多线程的引入(见第3章),冲突失效可能会增加,使得更高的关联度变得更具吸引力。
**第二种优化:通过方式预测来减少命中时间**
另一种方法是在减少冲突失效的同时保持直接映射缓存的命中速度。这种方法称为方式预测,它在缓存中保留额外的位来预测下一个缓存访问的方式(或集合内的块)。这种预测使得多路复用器可以提前设置,以选择所需的块,并在该时钟周期内,只有一个标签比较与读取缓存数据同时进行。未命中将导致在下一个时钟周期检查其他块以寻找匹配。
每个缓存块都增加了块预测位。这些位选择下一个缓存访问尝试的块。如果预测正确,缓存访问延迟为快速命中时间。如果预测错误,它会尝试其他块,改变方式预测器,增加一个额外的时钟周期延迟。模拟结果表明,对于二路集合关联缓存,方式预测的准确性超过90%,对于四路集合关联缓存为80%,并且I缓存的准确性优于D缓存。如果预测的速度至少比缓存访问快10%,则二路集合关联缓存的平均内存访问时间会更低,这种情况很可能发生。方式预测最早在1990年代中期的MIPS R10000中使用。在使用二路集合关联的处理器中很受欢迎,并且在多个ARM处理器中使用,这些处理器有四路集合关联缓存。对于非常快速的处理器,实现关键的一周期停顿以保持方式预测惩罚较小可能是具有挑战性的。
方式预测的扩展形式还可以通过使用方式预测位来减少功耗,以决定实际访问哪个缓存块(方式预测位实际上是额外的地址位);这种方法,可能称为方式选择,当方式预测正确时能节省功耗,但在方式错误预测时增加显著时间,因为不仅需要重复访问,还需要重新进行标签匹配和选择。这样的优化可能只有在低功耗处理器中才有意义。Inoue等(1999)估计,使用四路集合关联缓存的方式选择方法在SPEC95基准测试中,I缓存的平均访问时间增加了1.04,D缓存增加了1.13,但相对于正常的四路集合关联缓存,I缓存的平均功耗减少了0.28,D缓存减少了0.35。方式选择的一个显著缺点是它使得缓存访问的流水线化变得困难;然而,随着能源问题的加剧,不需要为整个缓存供电的方案变得越来越有意义。
**示例** 假设D-cache的访问次数是I-cache的一半,并且在正常的四路集合关联实现中,I-cache和D-cache分别占处理器功耗的25%和15%。根据前述研究的估算,确定方式选择是否能提高每瓦特的性能。
**回答** 对于I-cache,功耗节省为总功耗的25% × 0.28 = 0.07,而对于D-cache则为15% × 0.35 = 0.05,总共节省0.12。方式预测版本需要标准四路缓存功耗的0.88。缓存访问时间的增加是I-cache平均访问时间增加加上D-cache访问时间增加的一半,即1.04 + 0.5 × 0.13 = 1.11倍。这表明方式选择的性能为标准四路缓存的0.90。因此,方式选择每焦耳的性能稍微提高了,比例为0.90/0.88 = 1.02。该优化在功耗而非性能为主要目标的情况下效果最佳。
**第三种优化:流水线访问和多银行缓存以增加带宽**
这些优化通过流水线缓存访问或通过增加多个银行来拓宽缓存,以允许每个时钟周期进行多次访问,从而提高缓存带宽。这些优化是增加指令吞吐量的超流水线和超标量方法的对偶。这些优化主要针对L1缓存,因为在这里访问带宽限制了指令吞吐量。虽然L2和L3缓存也使用多个银行,但主要是作为节能管理技术。
对L1进行流水线化可以允许更高的时钟周期,但代价是增加了延迟。例如,在1990年代中期,Intel Pentium处理器的指令缓存访问流水线需要1个时钟周期;对于1990年代中期至2000年的Pentium Pro到Pentium III,流水线需要2个时钟周期;而对于2000年发布的Pentium 4和当前的Intel Core i7,则需要4个时钟周期。对指令缓存进行流水线化有效地增加了流水线阶段的数量,这会导致对错误预测分支的惩罚加大。相应地,对数据缓存进行流水线化会导致从发出加载指令到使用数据之间的时钟周期增加(见第3章)。今天,所有处理器都使用某种形式的L1流水线,即使只是为了简单地分开访问和命中检测,许多高速处理器则具有三层或更多层级的缓存流水线。
流水线处理指令缓存比数据缓存更容易,因为处理器可以依赖高性能的分支预测来限制延迟影响。许多超标量处理器可以在每个时钟周期发出并执行多个内存引用(常见的是加载或存储,有些处理器允许多个加载)。为了处理每个时钟周期的多个数据缓存访问,我们可以将缓存分成独立的银行,每个银行支持独立的访问。银行最初用于提高主内存的性能,现在也用于现代DRAM芯片和缓存中。Intel Core i7的L1缓存有四个银行(支持每个时钟周期最多2次内存访问)。
显然,banks的效果最佳时,当访问自然分布到各个银行时,因此地址到银行的映射会影响内存系统的行为。一个简单而有效的映射方法是将块的地址顺序地分布到各个银行,这被称为顺序交错。例如,如果有四个banks,银行0包含地址模4为0的所有块,银行1包含地址模4为1的所有块,图2.10显示了其相关性,以此类推。多个banks也可以减少缓存和DRAM的功耗。

图2.10 使用块地址的四路交错缓存banks。假设每个块64字节,每个地址都需要乘以64来得到字节地址。
多个banks在L2或L3缓存中也很有用,但原因不同。L2缓存中的多个banks可以处理多个未解决的L1缓存未命中,只要banks之间没有冲突。这是支持非阻塞缓存的关键能力。Intel Core i7的L2缓存有八个banks,而Arm Cortex处理器的L2缓存使用了1到4个banks。如前所述,多banks也可以减少能耗。
第四种优化:非阻塞缓存以增加缓存带宽
对于允许乱序执行的流水线计算机(在第3章中讨论),处理器在数据缓存未命中时不需要停顿。例如,处理器可以在等待数据缓存返回缺失数据的同时继续从指令缓存中提取指令。非阻塞缓存或无锁缓存通过允许数据缓存在未命中期间继续提供缓存命中,从而提升这种方案的潜在好处。这种“未命中的命中”优化通过在未命中期间继续提供帮助来减少有效未命中惩罚,而不是忽视处理器的请求。一个微妙而复杂的选项是缓存如果能够重叠多个未命中,可能会进一步降低有效未命中惩罚:一种“多重未命中的命中”或“未命中的未命中”优化。第二种选项只有在内存系统可以同时处理多个未命中时才有益;大多数高性能处理器(如Intel Core处理器)通常支持这两种选项,而许多低端处理器在L2中仅提供有限的非阻塞支持。
为了研究非阻塞缓存在减少缓存未命中惩罚方面的有效性,Farkas和Jouppi(1994)进行了研究,假设缓存为8 KiB,未命中惩罚为14个周期(适用于1990年代初期)。他们观察到,当允许一个未命中期间的命时时,SPECINT92基准测试的有效未命中惩罚减少了20%,SPECFP92基准测试减少了30%。
Li等(2011)更新了这项研究,使用了多级缓存、对未命中惩罚的更现代假设以及更大且要求更高的SPECCPU2006基准测试。该研究假设基于单个Intel i7核心(见第2.6节)运行SPECCPU2006基准测试。图2.11展示了在允许1、2和64个未命中期间的命中时数据缓存访问延迟的减少;标题中描述了内存系统的更多细节。与早期研究相比,由于缓存变得更大以及L3缓存的添加,SPECINT2006基准测试显示出平均约9%的缓存延迟减少,而SPECFP2006基准测试则约减少了12.5%。
例子:对于浮点程序,二路组相联和在一次未命中下的命中哪个更重要?对于整数程序呢?
假设32 KiB数据缓存的平均未命中率如下:浮点程序使用直接映射缓存的未命中率为5.2%,使用二路组相联缓存的未命中率为4.9%;整数程序使用直接映射缓存的未命中率为3.5%,使用二路组相联缓存的未命中率为3.2%。假设L2缓存的未命中惩罚为10个周期,L2缓存的未命中次数和惩罚相同。
回答:
对于浮点程序,平均内存停顿时间的计算如下:

二路组相联缓存的访问延迟(包括停顿时间)为 0.49/0.52,即直接映射缓存的94%。图2.11的标题指出,在一次未命中下的命中将浮点程序的平均数据缓存访问延迟降低到阻塞缓存的87.5%。因此,对于浮点程序来说,直接映射数据缓存支持一次未命中下的命中,相比于在未命中时阻塞的二路组相联缓存,性能更佳。

图2.11 通过允许在缓存未命中情况下进行1次、2次或64次命中来评估非阻塞缓存的有效性,左侧展示了9个SPECINT基准测试,右侧展示了9个SPECFP基准测试。该数据内存系统模拟了Intel i7处理器,包括一个32 KiB的L1缓存,访问延迟为四个周期。L2缓存(与指令共享)为256 KiB,访问延迟为10个时钟周期。L3缓存为2 MiB,访问延迟为36个周期。所有缓存均为八路组相联,块大小为64字节。在未命中情况下允许一次命中可以将整数基准测试的未命中惩罚降低9%,将浮点基准测试的未命中惩罚降低12.5%。允许第二次命中将这些结果改善到10%和16%,而允许64次命中则几乎没有额外的改进。
对于整数程序,计算方法如下:

二路组相联缓存的数据缓存访问延迟为 0.32/0.35,即直接映射缓存的91%,而允许一次未命中下的命中将访问延迟减少了9%,使得这两种选择的性能相当。
非阻塞缓存性能评估的真正难点在于,缓存未命中并不一定会使处理器停顿。在这种情况下,很难判断单次未命中的影响,从而计算平均内存访问时间。有效的未命中惩罚不是未命中的总和,而是处理器被阻塞的非重叠时间。非阻塞缓存的好处很复杂,因为它取决于多次未命中的惩罚、内存引用模式以及处理器在未命中时能够执行多少指令。
一般而言,乱序处理器能够隐藏大部分L1数据缓存未命中时的惩罚,如果未命中数据在L2缓存中,但对较低级缓存的未命中则难以隐藏较大部分的惩罚。决定支持多少个未命中需要考虑多种因素:
- 未命中流中的时间和空间局部性,这决定了未命中是否可以发起对低级缓存或内存的新访问。
- 响应内存或缓存的带宽。
- 在缓存的最低级别(未命中时间最长)允许更多的未命中需要在更高级别支持至少相同数量的未命中,因为未命中必须在最高级别缓存发起。
- 内存系统的延迟。
下面的简化示例说明了关键思想。
例子:假设主存访问时间为36纳秒,内存系统能够维持16 GiB/s的持续传输率。如果块大小为64字节,那么在假设我们可以保持峰值带宽给定请求流,并且访问之间没有冲突的情况下,我们需要支持的最大未命中数是多少?如果一个引用与之前四个引用中的任何一个发生冲突的概率是50%,并且我们假设访问必须等待直到先前的访问完成,估计最大未命中数。为了简化问题,忽略未命中之间的时间。
答案:在第一个情况下,假设我们能够维持峰值带宽,内存系统可以支持 (16 × 10^9) / 64 = 2.5 亿个引用每秒。由于每个引用需要36纳秒,我们可以支持 250 × 10^6 × 36 × 10^-9 = 9 个引用。
如果冲突的概率大于0,那么我们需要更多的未命中引用,因为我们无法开始处理那些发生冲突的引用;内存系统需要更多独立的引用,而不是更少!为了近似估计,我们可以简单地假设一半的内存引用不需要发给内存。这意味着我们必须支持两倍的未命中引用,即18个。
在Li、Chen、Brockman和Jouppi的研究中,他们发现整数程序的CPI(每条指令周期数)在一次未命中的情况下减少约7%,而在块大小为64字节时减少约12.7%。对于浮点程序,减少分别为一次未命中情况下的12.7%和块大小为64字节时的17.8%。这些减少值与图2.11中显示的数据缓存访问延迟减少情况相当接近。
实现非阻塞缓存
虽然非阻塞缓存有潜力提高性能,但实现起来并非简单。主要面临两个初步挑战:处理命中与未命中之间的争用,以及追踪未处理的未命中,以便了解何时可以继续进行加载或存储。首先考虑第一个问题。在阻塞缓存中,未命中会导致处理器停滞,直到未命中处理完成之前,不会有进一步的缓存访问。然而,在非阻塞缓存中,命中可能与从内存层次结构下一级返回的未命中发生冲突。如果允许多个未处理的未命中(几乎所有现代处理器都允许),未命中之间发生冲突是可能的。这些冲突必须解决,通常通过首先优先处理命中,其次按顺序处理冲突的未命中(如果可能的话)。
第二个问题是因为我们需要追踪多个未处理的未命中。在阻塞缓存中,我们始终知道哪个未命中正在返回,因为只有一个未命中可以处于处理状态。而在非阻塞缓存中,这种情况很少发生。乍看之下,你可能认为未命中总是按顺序返回,因此可以通过简单的队列来匹配返回的未命中与最长未处理请求。然而,考虑到在L1中发生的未命中,它可能在L2中生成命中或未命中;如果L2也是非阻塞的,那么未命中返回到L1的顺序可能与它们最初发生的顺序不同。多核和其他多处理器系统中不均匀的缓存访问时间也引入了这种复杂性。
当未命中返回时,处理器必须知道哪个加载或存储操作引起了未命中,以便该指令可以继续执行;同时,它必须知道数据应放置在缓存中的位置(以及该块的标签设置)。在现代处理器中,这些信息保存在一组寄存器中,通常称为未命中状态处理寄存器(MSHRs)。如果允许n个未处理的未命中,就会有n个MSHR,每个MSHR保存有关未命中在缓存中位置的信息、任何标签位的值以及引起未命中的加载或存储信息(在下一章中,你将看到如何追踪这些)。因此,当未命中发生时,我们为处理该未命中分配一个MSHR,输入有关未命中的信息,并用MSHR的索引标记内存请求。内存系统在返回数据时使用该标签,允许缓存系统将数据和标签信息转移到适当的缓存块,并“通知”生成未命中的加载或存储操作数据现在可用,可以继续操作。非阻塞缓存显然需要额外的逻辑,因此有一定的能量成本。然而,由于它们可能减少停滞时间,从而缩短执行时间并降低能量消耗,因此很难准确评估其能量成本。
除了上述问题外,多处理器内存系统(无论是在单个芯片内还是多个芯片上)还必须处理与内存一致性和一致性相关的复杂实现问题。此外,由于缓存未命中不再是原子的(因为请求和响应被拆分并可能在多个请求之间交错),可能会出现死锁。有关这些问题的详细信息,请参见在线附录I中的第I.7节。
第五种优化:优先访问关键字和早期重启以减少未命中惩罚
这种技术基于这样一个观察:处理器通常一次只需要块中的一个字。这个策略的核心在于“急躁”:不要等到整个块完全加载后再发送请求的字和重启处理器。以下是两种具体策略:
■ 关键字优先—首先从内存中请求未命中的字,并在其到达后立即将其发送给处理器;在填充块中的其他字时,让处理器继续执行。
■ 早期重启—按正常顺序获取字,但一旦请求的块中的字到达,将其发送给处理器,并让处理器继续执行。
通常,这些技术只有在块较大时才有利,因为如果块较小,效果不明显。请注意,缓存通常在填充剩余块时继续满足对其他块的访问。然而,由于空间局部性,下一次访问很可能是对块中剩余部分的访问。就像非阻塞缓存一样,未命中惩罚的计算并不简单。当关键字优先策略有第二个请求时,有效的未命中惩罚是从参考点到第二块到达的非重叠时间。关键字优先和早期重启的效果取决于块的大小以及对尚未提取的块部分的再次访问的可能性。例如,对于使用早期重启和关键字优先的i7 6700处理器运行的SPECint2006测试,平均每个块有超过一个引用(平均1.23次,范围从0.5到3.0)。我们将在第2.6节中更详细地探讨i7内存层次结构的性能。
第六种优化:合并写缓冲区以减少未命中惩罚
写直通缓存依赖于写缓冲区,因为所有写操作必须发送到层次结构的下一级。即使是写回缓存,在替换块时也会使用简单的缓冲区。如果写缓冲区为空,数据和完整地址将写入缓冲区,从处理器的角度来看,写操作完成;处理器继续工作,同时写缓冲区准备将字写入内存。如果缓冲区包含其他已修改的块,可以检查地址以查看新数据的地址是否与有效写缓冲区条目的地址匹配。如果匹配,新数据将与该条目合并。写合并就是这种优化的名称。英特尔Core i7等许多处理器使用写合并。
如果缓冲区已满且没有地址匹配,缓存(和处理器)必须等待直到缓冲区有空闲条目。这种优化更有效地使用内存,因为多字写入通常比逐字写入要快。Skadron和Clark(1997)发现,即使是合并的四条目写缓冲区也会产生导致5%-10%性能损失的停顿。
这种优化还减少了由于写缓冲区已满而导致的停顿。图2.12显示了有和没有写合并的写缓冲区。假设写缓冲区中有四个条目,每个条目可以容纳四个64位字。没有这种优化时,四个对顺序地址的写入将以每个条目一个字的方式填满缓冲区,即使这四个字合并后正好可以放入写缓冲区的一个条目中。

图2.12 在这个写合并的示意图中,顶部的写缓冲区不使用写合并,而底部的写缓冲区使用了写合并。四个写入操作在写合并的情况下合并为一个缓冲区条目;如果没有写合并,缓冲区已满,尽管每个条目的四分之三空间被浪费。缓冲区有四个条目,每个条目可容纳四个64位字。每个条目的地址在左侧,带有有效位(V),指示该条目中下一个顺序8字节是否被占用。(如果没有写合并,图上部右侧的字仅用于同时写入多个字的指令。)
需要注意的是,输入/输出设备寄存器通常映射到物理地址空间。这些I/O地址无法允许写合并,因为单独的I/O寄存器可能无法像内存中的字数组那样工作。例如,它们可能需要每个I/O寄存器一个地址和数据字,而不是使用单一地址进行多字写入。这些副作用通常通过标记页面为需要非合并直通写入的缓存来实现。
第七种优化:编译器优化以降低未命中率
到目前为止,我们的技术都需要更改硬件。下一种技术则在不改变任何硬件的情况下降低未命中率。
这种神奇的减少源于优化软件——硬件设计师最喜欢的解决方案!处理器与主存之间日益扩大的性能差距促使编译器开发者仔细研究内存层次结构,以查看编译时优化是否能提高性能。再次,研究分为对指令未命中的改进和对数据未命中的改进。接下来介绍的优化在许多现代编译器中都可以找到。
循环互换
某些程序具有嵌套循环,这些循环以非顺序的方式访问内存中的数据。简单地交换循环的嵌套顺序可以使代码按数据存储的顺序访问数据。假设数组无法完全放入缓存,这种技术通过改善空间局部性来减少未命中率;重新排序最大限度地利用缓存块中的数据,直到它们被丢弃。例如,如果 x 是一个大小为 [5000,100] 的二维数组,分配方式使得 x[i,j] 和 x[i,j+1] 是相邻的(这种排列方式称为行主序,因为数组是按行布局的),那么以下两段代码展示了如何优化访问:

原始代码会以每次跨越100个字的方式跳过内存,而修订后的版本在进入下一个缓存块之前会访问一个缓存块中的所有字。这种优化在不影响执行指令数量的情况下提高了缓存性能。
阻塞优化
这种优化提高了时间局部性以减少未命中率。我们再次处理多个数组,其中一些数组按行访问,另一些按列访问。按行(行主序)或按列(列主序)存储数组并不能解决问题,因为在每次循环迭代中都会使用行和列。这种正交访问意味着像循环互换这样的变换仍然有很大的改进空间。
阻塞算法不是在整个数组的行或列上操作,而是在子矩阵或块上操作。其目标是在数据被替换之前,最大限度地访问已加载到缓存中的数据。下面的代码示例展示了执行矩阵乘法的过程,有助于激励这种优化:

两个内层循环读取z的所有N-by-N元素,重复读取y中同一行的N个元素,并写入x的一行N个元素。图2.13展示了对这三个数组的访问快照。深色表示最近的访问,浅色表示较旧的访问,白色表示尚未访问。容量未命中次数显然取决于N和缓存大小。如果缓存能够容纳所有三个N-by-N矩阵,那么一切正常,前提是没有缓存冲突。如果缓存可以容纳一个N-by-N矩阵和一行N元素,那么至少y的第i行和数组z可能会保留在缓存中。少于此,x和z都可能发生未命中。在最坏情况下,进行N³次操作时将访问2N³ + N²个内存字。
为了确保被访问的元素能够适应缓存,原始代码被修改为在B by B的子矩阵上计算。两个内层循环现在以B为步长计算,而不是x和z的完整长度。B被称为阻塞因子。(假设x初始化为零。)

图2.14展示了使用阻塞技术对三个数组的访问。仅考虑容量未命中,总共访问的内存字数为2N³/B + N²。这个总数大约提高了B倍。因此,阻塞利用了空间局部性和时间局部性的结合,因为y受益于空间局部性,而z受益于时间局部性。虽然我们的示例使用了正方形块(B x B),但如果矩阵不是正方形,我们也可以使用矩形块。

虽然我们旨在减少缓存未命中,但阻塞还可以用于帮助寄存器分配。通过选择一个小的阻塞大小,以便块可以保留在寄存器中,我们可以最小化程序中的加载和存储次数。正如我们将在第4章第4.8节中看到的,缓存阻塞对于从基于缓存的处理器中获得良好性能是绝对必要的,尤其是在使用矩阵作为主要数据结构的应用程序中。
### 第八个优化:硬件预取指令和数据以减少未命中惩罚或未命中率
非阻塞缓存通过重叠执行和内存访问,有效减少了未命中惩罚。另一种方法是在处理器请求之前预取数据和指令。这些预取可以直接放入缓存,或者放入一个比主内存更快访问的外部缓冲区。
指令预取通常是在缓存外的硬件中完成。通常情况下,处理器在未命中时会获取两个块:请求的块和下一个连续的块。请求的块在返回时被放入指令缓存,而预取的块则放入指令流缓冲区。如果请求的块已经在指令流缓冲区中,则原始缓存请求会被取消,从流缓冲区读取该块,并发出下一个预取请求。
类似的方法也可以应用于数据访问(Jouppi, 1990)。Palacharla和Kessler(1994)研究了一组科学程序,考虑了能够处理指令或数据的多个流缓冲区。他们发现,八个流缓冲区可以捕获来自具有两个64 KiB四路组相联缓存的处理器(一个用于指令,另一个用于数据)50%-70%的所有未命中。
Intel Core i7支持对L1和L2的硬件预取,最常见的预取情况是访问下一行。一些早期的Intel处理器使用了更激进的硬件预取,但这导致某些应用程序性能下降,促使一些高级用户关闭该功能。
图2.15显示了当启用硬件预取时,SPEC2000程序子集的整体性能提升。请注意,该图仅包括12个整数程序中的2个,而涵盖了大多数SPECCPU浮点程序。我们将在第2.6节中回到对i7上预取的评估。

图2.15 显示了在启用硬件预取的情况下,Intel Pentium 4 对12个SPECint2000基准测试中2个程序和14个SPECfp2000基准测试中9个程序的加速效果。仅显示了最能从预取中受益的程序;对其余15个SPECCPU基准测试的预取加速效果低于15%(Boggs等,2004)。
预取依赖于利用本来未使用的内存带宽,但如果干扰了需求未命中,实际上可能降低性能。编译器的帮助可以减少无效预取。当预取效果良好时,对功耗的影响可以忽略不计。然而,当预取的数据未被使用,或有用数据被替换时,预取将对功耗产生非常负面的影响。
### 第九个优化:编译器控制的预取以减少未命中惩罚或未命中率
硬件预取的替代方案是让编译器插入预取指令,以在处理器需要数据之前请求数据。预取有两种形式:
- **寄存器预取**将值加载到寄存器中。
- **缓存预取**仅将数据加载到缓存中,而不进入寄存器。
这两者可以是故障型或非故障型,即地址可能会或不会引发虚拟地址错误和保护违规。用这种术语来说,普通加载指令可以视为“故障寄存器预取指令”。非故障预取在正常情况下会导致异常时,会变为无操作指令,这正是我们希望的。
最有效的预取是“语义上不可见”的:它不改变寄存器和内存的内容,也不会导致虚拟内存故障。目前大多数处理器提供非故障缓存预取。本节假设使用非故障缓存预取,也称为非绑定预取。
预取只有在处理器能够在预取数据的同时继续执行时才有意义;即,缓存不会停滞,而是继续提供指令和数据,同时等待预取数据返回。正如预期的那样,这类计算机的数据缓存通常是非阻塞的。
与硬件控制的预取类似,目标是使执行与数据预取重叠。循环是重要的目标,因为它们适合进行预取优化。如果未命中惩罚较小,编译器只需展开循环一次或两次,并将预取与执行调度。如果未命中惩罚较大,则使用软件流水线技术(参见附录H)或多次展开,以为未来的迭代预取数据。
然而,发出预取指令会产生指令开销,因此编译器必须小心确保这些开销不会超过收益。通过集中关注可能发生缓存未命中的引用,程序可以避免不必要的预取,同时显著改善平均内存访问时间。
示例:对于以下代码,确定哪些访问可能导致数据缓存未命中。接下来,插入预取指令以减少未命中。最后,计算执行的预取指令数量以及通过预取避免的未命中次数。假设我们有一个8 KiB的直接映射数据缓存,块大小为16字节,并且它是一个写回缓存,采用写分配策略。数组a和b的元素为8字节长,因为它们是双精度浮点数组。数组a有3行100列,数组b有101行3列。假设它们在程序开始时不在缓存中。

回答:编译器首先会确定哪些访问可能导致缓存未命中;否则,我们将浪费时间为会命中的数据发出预取指令。数组a的元素按照存储顺序写入,因此会受益于空间局部性:偶数j会未命中,奇数j会命中。由于a有3行100列,其访问将导致150次未命中。
数组b不受益于空间局部性,因为访问顺序不一致。但数组b在时间局部性上受益两次:每次i的迭代访问相同元素,而每次j的迭代使用与上次相同的b值。忽略潜在的冲突未命中,b的未命中将是i=0时b[j + 1][0]的访问,以及j=0时b[j][0]的首次访问。因为当i=0时,j从0到99,访问b将导致101次未命中。
因此,该循环将导致a约150次未命中,加上b的101次,或总计251次未命中。为了简化优化,我们不考虑循环的首次访问是否需要预取。这些可能已经在缓存中,或者我们将面临a或b的前几个元素的未命中惩罚。我们也不担心循环结束时试图超出a(a[i][100] … a[i][106])和b(b[101][0] … b[107][0])的预取。如果这些是错误的预取,我们就不能这么奢侈。假设未命中惩罚非常大,我们需要至少提前七次迭代开始预取。(换句话说,我们假设在第八次迭代之前,预取没有任何好处。)我们将下划线标出添加预取所需的代码更改。


这段修订后的代码预取了a[i][7]到a[i][99]和b[7][0]到b[100][0],将未预取的未命中次数减少到:
- 第一个循环中b[0][0]、b[1][0]、…、b[6][0]的7次未命中
- 第一个循环中a[0][0]、a[0][1]、…、a[0][6]的4次未命中(空间局部性将未命中减少到每个16字节缓存块1次)
- 第二个循环中a[1][0]、a[1][1]、…、a[1][6]的4次未命中
- 第二个循环中a[2][0]、a[2][1]、…、a[2][6]的4次未命中
总共为19次未预取的未命中。避免232次缓存未命中的代价是执行400条预取指令,这可能是一个不错的权衡。
例子:计算前面例子的节省时间。忽略指令缓存未命中,假设数据缓存中没有冲突或容量未命中。假设预取可以与彼此和缓存未命中重叠,从而以最大内存带宽传输。以下是忽略缓存未命中的关键循环时间:原始循环每次迭代需要7个时钟周期,第一个预取循环每次迭代需要9个时钟周期,第二个预取循环每次迭代需要8个时钟周期(包括外部for循环的开销)。未命中需要100个时钟周期。
答案:原始的双重嵌套循环执行乘法3 × 100或300次。由于循环每次迭代需要7个时钟周期,总共为300 × 7或2100个时钟周期,加上缓存未命中。缓存未命中增加了251 × 100或25,100个时钟周期,总计27,200个时钟周期。第一个预取循环迭代100次;每次迭代9个时钟周期,总共为900个时钟周期,加上缓存未命中的1100个时钟周期,得到总计2000个时钟周期。第二个循环执行2 × 100或200次,每次迭代8个时钟周期,总共需要1600个时钟周期,加上800个时钟周期的缓存未命中,总计2400个时钟周期。从之前的例子中我们知道,这段代码在执行这两个循环的4400个时钟周期中执行了400条预取指令。如果我们假设预取与其余执行完全重叠,则预取代码的速度是27,200/4400,约为6.2倍。
虽然数组优化易于理解,但现代程序更可能使用指针。Luk 和 Mowry(1999)证明,基于编译器的预取有时也可以扩展到指针。在10个具有递归数据结构的程序中,当访问一个节点时预取所有指针,使得一半的程序性能提高了4%到31%。另一方面,其余程序的性能仍在原始性能的2%之内。关键问题在于预取是否针对已经在缓存中的数据,以及预取是否发生得足够早,以便数据能在需要时到达。
许多处理器支持缓存预取指令,高端处理器(如Intel Core i7)通常还会在硬件中进行某种类型的自动预取。
第十个优化:使用高带宽内存(HBM)扩展内存层次结构
由于大多数服务器中的通用处理器可能需要比HBM封装所能提供的更多内存,因此建议使用封装内的DRAM构建大规模L4缓存,未来技术的容量范围从128 MiB到1 GiB及以上,远超过当前的片上L3缓存。使用如此大的基于DRAM的缓存会引发一个问题:标签存储在哪里?这取决于标签的数量。假设使用64B块大小,那么1 GiB的L4缓存需要96 MiB的标签,这远超过CPU缓存中的静态存储。将块大小增大到4 KiB,会显著减少标签存储至256K条目,总存储量不足1 MiB,这在下一代多核处理器中可能是可以接受的,因为L3缓存通常为4–16 MiB或更多。然而,这样的大块大小存在两个主要问题。
首先,当许多块的内容不被需要时,缓存可能会被低效使用,这被称为碎片化问题,这在虚拟内存系统中也会发生。此外,传输如此大的块如果其中许多数据未被使用也是低效的。第二,由于块大小较大,DRAM缓存中持有的不同块数量显著减少,这可能导致更多的未命中,特别是冲突未命中和一致性未命中。
解决第一个问题的一个部分方案是添加子锁定(subblocking)。子锁定允许块的部分内容无效,要求在未命中时进行获取。然而,子锁定对第二个问题没有任何解决作用。
标签存储是使用较小块大小的主要缺点。一个可能的解决方案是将L4的标签存放在HBM中。乍一看,这似乎不可行,因为每次L4访问需要对DRAM进行两次访问:一次用于标签,一次用于数据。由于随机DRAM访问的长延迟,通常需要100个或更多的处理器时钟周期,因此这种方法曾被放弃。Loh和Hill(2011)提出了一种巧妙的解决方案:将标签和数据存放在HBM SDRAM的同一行中。尽管打开(并最终关闭)行需要较长时间,但访问行中不同部分的CAS延迟约为新行访问时间的三分之一。因此,我们可以首先访问块的标签部分,如果命中,则通过列访问选择正确的数据字。Loh和Hill(L-H)建议将L4 HBM缓存组织为每个SDRAM行包含一组标签(位于块头部)和29个数据段,形成29路集合关联缓存。当访问L4时,适当的行被打开并读取标签;命中时只需再进行一次列访问即可获取匹配的数据。
Qureshi和Loh(2012)提出了一种名为合金缓存的改进,减少命中时间。合金缓存将标签和数据结合在一起,使用直接映射的缓存结构。这使得L4的访问时间减少到一个HBM周期,通过直接索引HBM缓存并进行标签和数据的突发传输。图2.16显示了合金缓存、L-H方案和基于SRAM的标签的命中延迟。合金缓存的命中延迟比L-H方案减少了超过2倍,但未命中率增加了1.1到1.2倍。基准测试的选择在图注中有所说明。

图2.16显示了L-H方案、当前不切实际的使用SRAM作为标签的方案以及合金缓存组织的平均命中时间延迟(以时钟周期为单位)。在SRAM情况下,我们假设SRAM的访问时间与L3相同,并且在访问L4之前会进行检查。平均命中延迟为43(合金缓存)、67(SRAM标签)和107(L-H)。这里使用的10个SPECCPU2006基准测试是内存密集型的;如果L3完美,每个基准的运行速度将提高一倍。
不幸的是,在这两种方案中,未命中都需要两次完整的DRAM访问:一次用于获取初始标签,另一次用于访问主存(速度更慢)。如果我们能加快未命中检测的速度,就能减少未命中时间。为了解决这个问题,提出了两种不同的解决方案:一种使用映射表来跟踪缓存中的块(仅记录块是否存在,而非其位置);另一种使用内存访问预测器,通过历史预测技术预测可能的未命中,类似于全局分支预测中使用的方法(见下一章)。研究表明,小型预测器可以高精度地预测可能的未命中,从而降低整体未命中惩罚。
图2.17显示了在图2.16中使用的内存密集基准测试中,SPECrate获得的加速效果。合金缓存方法优于LH方案,甚至优于不切实际的SRAM标签,因为未命中预测器的快速访问时间与良好的预测结果结合,导致预测未命中的时间更短,从而降低未命中惩罚。合金缓存的表现接近理想情况,即具有完美未命中预测和最小命中时间的L4。

图2.17显示了在LH方案、SRAM标签方案和理想L4(Ideal)下运行SPECrate基准测试的性能加速情况;加速比为1表示L4缓存没有改进,加速比为2则意味着如果L4完美且没有访问时间,将能够实现。使用了10个内存密集型基准测试,每个基准测试运行八次。所采用的未命中预测方案也一同使用。理想情况下假设仅需访问和传输L4中请求的64字节块,并且L4的预测准确性是完美的(即,所有未命中在零成本下已知)。
HBM可能会在多种不同配置中得到广泛应用,从作为一些高性能专用系统的整个内存系统,到作为较大服务器配置的L4缓存。
缓存优化总结  
提高命中时间、带宽、未命中惩罚和未命中率的技术通常会影响平均内存访问方程的其他组件以及内存层次的复杂性。图2.18总结了这些技术并估计了对复杂性的影响,其中“+”表示该技术改善了某个因素,“%”表示该因素受到影响,空白表示没有影响。通常,没有任何技术在多个类别上都能提供帮助。

图2.18 显示了10种高级缓存优化的总结,体现了对缓存性能、功耗和复杂性的影响。尽管通常一个技术只对一个因素有帮助,但如果预取足够提前进行,可以减少未命中;如果没有做到这一点,则可以降低未命中惩罚。"+"表示该技术改善了该因素,"%"表示对该因素造成负面影响,空白表示没有影响。复杂性度量是主观的,0表示最简单,3表示具有挑战性。
2.4 Virtual Memory and Virtual Machines
虚拟机被视为真实机器的高效、隔离的副本。我们通过虚拟机监视器(VMM)的概念来解释这些观点……VMM有三个基本特征。首先,VMM为程序提供的环境与原始机器基本相同;其次,在该环境中运行的程序在速度上最多仅有轻微下降;最后,VMM对系统资源具有完全控制权。
——杰拉尔德·波佩克和罗伯特·戈德堡,《可虚拟化的第三代架构的正式要求》,《计算机协会通讯》(1974年7月)。
附录B的B.4节描述了虚拟内存的关键概念。虚拟内存允许将物理内存视为二级存储(可能是磁盘或固态硬盘)的缓存。虚拟内存在内存层次结构的两个级别之间移动页面,就像缓存在不同层次之间移动块一样。TLB作为页表的缓存,消除了每次地址转换时进行内存访问的需要。虚拟内存还提供了共享同一物理内存但拥有独立虚拟地址空间的进程之间的隔离。读者应确保理解虚拟内存的这两项功能后再继续。
本节重点讨论共享同一处理器的进程之间的保护和隐私问题。安全性和隐私是2017年信息技术面临的最棘手挑战之一。电子盗窃事件频繁发生,通常涉及信用卡号码的列表,且据信还有更多未报告的案件。当然,这些问题源于编程错误,允许网络攻击访问不该访问的数据。编程错误是常态,随着现代复杂软件系统的发展,这种情况时有发生。因此,研究人员和从业者都在寻找改进计算系统安全性的途径。虽然保护信息不仅限于硬件,但在我们看来,真正的安全性和隐私可能涉及计算机架构和系统软件的创新。
本节首先回顾通过虚拟内存保护进程之间相互隔离的架构支持,接着描述虚拟机提供的额外保护、虚拟机的架构要求以及虚拟机的性能。如第六章所示,虚拟机是云计算的基础技术。
### 通过虚拟内存实现保护
基于页面的虚拟内存,包括缓存页面表条目的TLB,是保护进程相互隔离的主要机制。附录B的B.4和B.5节回顾了虚拟内存,详细描述了在80x86架构下通过分段和分页实现的保护。本节提供了快速回顾;如果过于简略,请参考附录B中标明的部分。
多道程序设计允许多个程序同时运行在同一计算机上,这导致了对程序之间保护和共享的需求,以及“进程”这一概念的提出。从比喻上讲,进程是一个程序的呼吸空气和生活空间,即正在运行的程序及其继续运行所需的所有状态。在任何时刻,都必须能够从一个进程切换到另一个进程。这种交换称为进程切换或上下文切换。
操作系统和体系结构共同努力,使得进程能够共享硬件而不相互干扰。为此,体系结构必须限制用户进程运行时可以访问的内容,同时允许操作系统进程访问更多资源。至少,体系结构必须做到以下几点:
### 1. 提供至少两种模式,以指示正在运行的进程是用户进程还是操作系统进程。后者有时称为内核进程或监控进程。
### 2. 提供一部分处理器状态,供用户进程使用但不可写。这部分状态包括用户/监控模式位、异常使能/禁用位和内存保护信息。用户被禁止写入此状态,因为如果用户能够赋予自己监控权限、禁用异常或更改内存保护,操作系统将无法控制用户进程。
### 3. 提供机制,使处理器能够从用户模式切换到监控模式,反之亦然。通常,向监控模式的切换通过系统调用实现,该调用作为一种特殊指令,将控制权转移到监控代码空间中的特定位置。程序计数器(PC)会保存系统调用时的点,处理器进入监控模式。返回用户模式的过程类似于子程序返回,恢复之前的用户/监控模式。
### 4. 提供机制限制内存访问,以保护进程的内存状态,而无需在上下文切换时将进程交换到磁盘上。
附录 A 描述了几种内存保护方案,但迄今为止最流行的方案是为每个虚拟内存页面添加保护限制。固定大小的页面,通常为 4 KiB、16 KiB 或更大,通过页表从虚拟地址空间映射到物理地址空间。保护限制包含在每个页表项中。这些保护限制可能决定用户进程是否可以读取该页面、是否可以写入该页面,以及是否可以从该页面执行代码。此外,如果某个页面不在页表中,进程将无法读取或写入该页面。由于只有操作系统可以更新页表,因此分页机制提供了完全的访问保护。
分页虚拟内存意味着每次内存访问逻辑上至少需要两次时间,一次访问用于获取物理地址,第二次访问用于获取数据。这种成本将是过于昂贵的。解决方案是依赖局部性原则;如果访问具有局部性,那么访问的地址转换也必须具有局部性。通过将这些地址转换保存在特殊的缓存中,内存访问很少需要第二次访问来转换地址。这个特殊的地址转换缓存被称为 TLB。
TLB 条目类似于缓存条目,其中标签保存虚拟地址的部分,而数据部分保存物理页面地址、保护字段、有效位、以及通常的使用位和脏位。操作系统通过更改页表中的值并使相应的 TLB 条目无效来更改这些位。当从页表重新加载条目时,TLB 会获得这些位的准确副本。
假设计算机忠实地遵守页面限制并将虚拟地址映射到物理地址,似乎我们已经完成了。然而,新闻头条表明情况并非如此。
我们尚未完成的原因在于我们依赖于操作系统和硬件的准确性。如今的操作系统由数千万行代码组成。由于错误通常按每千行代码的数量来衡量,因此在生产操作系统中存在成千上万的漏洞。操作系统中的缺陷导致了常常被利用的安全漏洞。
这一问题以及未能执行保护可能带来的成本远高于过去的可能性,促使一些人寻求比完整操作系统更小代码库的保护模型,例如虚拟机。
### 通过虚拟机实现保护
与虚拟内存相关的一个几乎同样古老的概念是虚拟机(VMs)。它们在20世纪60年代末首次被开发,并在多年来一直是大型计算机的重要组成部分。尽管在1980年代和1990年代的单用户计算机领域几乎被忽视,但由于以下原因,它们最近重新获得了关注:
- 现代系统中隔离和安全性的重要性日益增加;
- 标准操作系统在安全性和可靠性方面的失败;
- 在数据中心或云计算中,多个无关用户共享单台计算机;
- 处理器原始速度的显著提升,使得虚拟机的开销更加可接受。
虚拟机(VMs)的最广泛定义基本上包括所有提供标准软件接口的仿真方法,例如Java虚拟机。我们关注的是在二进制指令集架构(ISA)级别提供完整系统级环境的虚拟机。通常,虚拟机支持与底层硬件相同的ISA,但也可以支持不同的ISA,这种方法常用于在ISA迁移期间,使来自旧ISA的软件能够在新ISA移植之前继续使用。我们这里关注的是虚拟机和底层硬件的ISA相匹配的情况。这种虚拟机称为(操作)系统虚拟机,如IBM VM/370、VMware ESX Server和Xen等。它们给用户呈现出拥有整台计算机的幻觉,包括操作系统的副本。单台计算机可以运行多个虚拟机,并支持多种不同的操作系统。在传统平台上,单个操作系统“拥有”所有硬件资源,但在虚拟机的情况下,多个操作系统共享硬件资源。
支持虚拟机的软件称为虚拟机监控器(VMM)或管理程序;VMM是虚拟机技术的核心。底层硬件平台称为主机,其资源在客虚拟机之间共享。VMM决定如何将虚拟资源映射到物理资源:物理资源可以是时间共享、分区,甚至在软件中仿真。VMM的体积比传统操作系统小得多;VMM的隔离部分可能只有大约10,000行代码。
一般而言,处理器虚拟化的成本取决于工作负载。用户级的处理器绑定程序(如SPECCPU2006)几乎没有虚拟化开销,因为操作系统很少被调用,因此一切以本地速度运行。相反,I/O密集型工作负载通常也是操作系统密集型的,并执行许多系统调用(I/O操作所需)和特权指令,这可能导致高虚拟化开销。开销由VMM必须仿真的指令数量和仿真的速度决定。因此,当客虚拟机与主机运行相同的ISA时,架构和VMM的目标是几乎所有指令都直接在本地硬件上运行。另一方面,如果I/O密集型工作负载也是I/O绑定,处理器虚拟化的成本可能会因处理器利用率低而被完全掩盖,因为它常常在等待I/O。
尽管我们这里的兴趣在于改善保护的虚拟机,但虚拟机还提供两个其他具有商业意义的好处:
1. 管理软件——虚拟机提供了一种抽象,可以运行完整的软件栈,甚至包括旧操作系统如DOS。典型的部署可能包括一些运行遗留操作系统的虚拟机,许多运行当前稳定版本的操作系统,以及一些测试下一个操作系统版本的虚拟机。
2. 管理硬件——多个服务器的一个原因是每个应用程序在不同计算机上运行其兼容版本的操作系统,这种分离可以提高可靠性。虚拟机允许这些独立的软件栈共享硬件,从而减少服务器数量。另一个例子是,大多数较新的虚拟机监控器支持将正在运行的虚拟机迁移到不同计算机,以平衡负载或从故障硬件中转移。云计算的兴起使得将整个虚拟机切换到另一个物理处理器的能力越来越有用。
这两个原因是云服务器(如亚马逊的)依赖虚拟机的原因。
### 虚拟机监控器的要求
虚拟机监控器必须做什么?它为客户软件提供一个软件接口,必须将客户的状态相互隔离,并保护自己免受客户软件(包括客户操作系统)的影响。定性要求如下:
- 客户软件在虚拟机上应表现得与在本地硬件上运行时完全相同,除了与性能相关的行为或多个虚拟机共享的固定资源的限制。
- 客户软件不得直接更改真实系统资源的分配。
为了“虚拟化”处理器,虚拟机监控器必须控制几乎所有内容——对特权状态、地址转换、输入/输出、异常和中断的访问,即使当前运行的是客户虚拟机和操作系统。例如,在定时器中断的情况下,虚拟机监控器会暂停当前运行的客户虚拟机,保存其状态,处理中断,确定下一个要运行的客户虚拟机,然后加载其状态。依赖定时器中断的客户虚拟机由虚拟机监控器提供虚拟定时器和模拟定时器中断。
为了掌控一切,虚拟机监控器的特权级别必须高于客户虚拟机,而客户虚拟机通常在用户模式下运行;这还确保了任何特权指令的执行将由虚拟机监控器处理。系统虚拟机的基本要求与前面提到的分页虚拟内存几乎相同:
- 至少有两种处理器模式:系统模式和用户模式。
- 仅在系统模式下可用的特权指令子集,如果在用户模式下执行,则会导致陷阱。所有系统资源必须仅通过这些指令进行控制。
### 指令集架构对虚拟机的支持
如果在指令集架构(ISA)的设计中考虑了虚拟机(VM),那么减少虚拟机监控器(VMM)必须执行的指令数量以及仿真这些指令所需的时间就相对容易。能够让虚拟机直接在硬件上执行的架构被称为可虚拟化的,IBM 370架构自豪地拥有这一标签。
然而,由于虚拟机最近才被考虑用于桌面和基于PC的服务器应用,许多指令集是在没有考虑虚拟化的情况下创建的。这些问题的根源包括80x86和大多数早期的RISC架构,尽管后者相比80x86架构问题较少。近期对x86架构的扩展试图弥补早期的不足,而RISC-V则明确包括了对虚拟化的支持。
由于VMM必须确保客户系统仅与虚拟资源交互,因此常规的客户操作系统作为用户模式程序在VMM之上运行。如果客户操作系统试图通过特权指令访问或修改与硬件资源相关的信息,例如读取或写入页表指针,它将会陷阱到VMM。VMM随后可以对相应的真实资源进行适当的更改。
因此,如果任何试图读取或写入此类敏感信息的指令在用户模式下执行时陷阱,VMM可以拦截它,并以客户操作系统所期望的方式支持该敏感信息的虚拟版本。
在缺乏此类支持的情况下,必须采取其他措施。VMM必须特别注意定位所有问题指令,并确保它们在客户操作系统执行时表现正确,从而增加了VMM的复杂性并降低了运行虚拟机的性能。第2.5节和第2.7节提供了80x86架构中问题指令的具体示例。
一个有吸引力的扩展允许虚拟机和操作系统在不同的特权级别上运行,每个级别都与用户级别不同。通过引入一个额外的特权级别,一些操作系统操作——例如,超出用户程序所授予权限但不需要VMM干预的操作(因为它们不会影响其他虚拟机)——可以直接执行,而无需陷阱和调用VMM的开销。我们将在后面讨论的Xen设计利用了三个特权级别。
虚拟机对虚拟内存和I/O的影响
另一个挑战是虚拟内存的虚拟化,因为每个虚拟机中的来宾操作系统管理自己的一组页表。为了解决这个问题,虚拟机监控器(VMM)将真实内存与物理内存区分开,将真实内存作为虚拟内存和物理内存之间的独立中间层。来宾操作系统通过其页表将虚拟内存映射到真实内存,而VMM的页表则将来宾的真实内存映射到物理内存。虚拟内存架构通常通过页表或TLB结构来指定。
为了避免每次内存访问都带来额外的间接层,VMM维护一个影子页表,直接从来宾虚拟地址空间映射到物理地址空间。通过检测对来宾页表的所有修改,VMM确保硬件用于转换的影子页表条目与来宾操作系统环境的条目相对应。因此,VMM必须拦截来宾操作系统对其页表的任何修改尝试。这通常通过写保护来宾页表来实现,任何对页表指针的访问都会被捕获。
IBM 370架构在1970年代通过VMM管理的额外间接层解决了页表问题。来宾操作系统仍然保持其页表,因此影子页是多余的。AMD也为其80x86实现了类似的方案。
在许多RISC计算机中,VMM管理真实的TLB,并保留每个来宾虚拟机的TLB内容副本。访问TLB的任何指令必须被捕获。带有进程ID标签的TLB可以支持来自不同虚拟机和VMM的条目的混合,从而避免在虚拟机切换时刷新TLB。同时,VMM在后台支持虚拟进程ID与真实进程ID之间的映射。
架构中最后要虚拟化的部分是I/O。这是系统虚拟化中最困难的部分,因为连接到计算机的I/O设备数量和种类日益增加。另一个难点是多个虚拟机之间共享真实设备,并且支持各种设备驱动程序也是一大挑战,尤其是在同一虚拟机系统上支持不同的来宾操作系统时。通过给每个虚拟机提供每种I/O设备驱动程序的通用版本,可以维持虚拟机的幻象,而让VMM处理真实的I/O。
虚拟到物理I/O设备的映射方法取决于设备类型。例如,物理磁盘通常由VMM进行分区,以创建来宾虚拟机的虚拟磁盘,VMM维护虚拟轨道和扇区与物理轨道和扇区之间的映射。网络接口通常在虚拟机之间以非常短的时间片共享,VMM的工作是跟踪虚拟网络地址的消息,以确保来宾虚拟机仅接收其专属的消息。
扩展指令集以实现高效虚拟化和更好的安全性
在过去的5到10年里,处理器设计师,包括AMD和Intel(以及在较小程度上ARM),引入了指令集扩展,以更有效地支持虚拟化。主要的性能提升体现在两个方面:处理页表和TLB(虚拟内存的基石)以及I/O,特别是在处理中断和DMA方面。通过避免不必要的TLB刷新,并使用嵌套页表机制(IBM几十年前采用的技术),而不是完全的影子页表集合,增强了虚拟内存的性能(参见附录L中的L.7节)。为了提高I/O性能,添加了架构扩展,允许设备直接使用DMA进行数据移动(消除了VMM可能导致的复制),并允许设备中断和命令由客户操作系统直接处理。这些扩展在内存管理或I/O密集型应用程序中显示出显著的性能提升。
随着公共云系统在关键应用中的广泛采用,关于这些应用中数据安全的担忧日益增加。任何能够访问比必须保密数据更高特权级别的恶意代码都会危及系统。例如,如果你正在运行一个信用卡处理应用,你必须绝对确保恶意用户无法访问信用卡号码,即使他们使用的是相同的硬件并故意攻击操作系统或虚拟机监视器(VMM)。通过使用虚拟化,我们可以防止外部用户访问不同虚拟机中的数据,这相比于多程序环境提供了显著的保护。然而,如果攻击者攻陷了VMM,或者通过观察另一VMM获取信息,这种保护可能仍然不足。例如,假设攻击者侵入了VMM;攻击者可以重新映射内存,从而访问任何数据部分。
另外,攻击可能依赖于引入到代码中的木马(参见附录B),该木马可以访问信用卡。由于木马与信用卡处理应用运行在同一虚拟机中,它只需要利用操作系统的漏洞就能访问关键数据。大多数网络攻击都使用了某种形式的木马,通常是利用操作系统漏洞,使得攻击者能够在保持CPU仍处于特权模式的同时恢复访问,或者允许攻击者上传并执行代码,仿佛它是操作系统的一部分。在这两种情况下,攻击者都获得了CPU的控制权,并利用更高的特权模式访问虚拟机内的任何内容。需要注意的是,仅靠加密并不能阻止这种攻击。如果内存中的数据是未加密的(这很常见),那么攻击者就能访问所有这些数据。此外,如果攻击者知道加密密钥存储的位置,他们可以自由访问密钥,从而访问任何加密数据。
最近,英特尔推出了一组指令集扩展,称为软件保护扩展(SGX),允许用户程序创建安全区,这些代码和数据块始终保持加密状态,只有在使用时才会解密,并且只有使用用户代码提供的密钥才能解密。由于安全区始终保持加密,标准的操作系统操作对于虚拟内存或I/O可以访问安全区(例如,移动一个页面),但无法提取任何信息。为了使安全区正常工作,所有必需的代码和数据必须是安全区的一部分。尽管更细粒度保护的话题已讨论了几十年,但由于其高开销以及其他更高效、干扰性更小的解决方案的可接受性,之前并未获得太多关注。网络攻击的增加和在线机密信息的数量促使人们重新审视提高这种细粒度安全性的技术。像英特尔的SGX一样,IBM和AMD近期的处理器也支持内存的动态加密。
一个示例虚拟机监视器:Xen虚拟机
在虚拟机(VM)早期开发过程中,许多低效问题逐渐显现。例如,来宾操作系统(OS)管理其虚拟到实际页面的映射,但这一映射被虚拟机监视器(VMM)忽略,后者执行实际的物理页面映射。换句话说,为了让来宾操作系统满意,耗费了大量不必要的精力。为了减少这种低效,VMM开发者决定,允许来宾操作系统意识到它正在运行于虚拟机上可能是值得的。例如,来宾操作系统可以假设物理内存与其虚拟内存一样大,从而无需进行内存管理。
允许对来宾操作系统进行小修改以简化虚拟化的做法被称为准虚拟化(paravirtualization),开源的Xen VMM就是一个很好的例子。Xen VMM被用于亚马逊的网络服务数据中心,为来宾操作系统提供了类似于物理硬件的虚拟机抽象,但去掉了许多麻烦的部分。例如,为了避免刷新翻译后备缓冲区(TLB),Xen将自己映射到每个虚拟机地址空间的上64 MiB中。Xen允许来宾操作系统分配页面,只需检查确保来宾操作系统不违反保护限制即可。为了保护来宾操作系统免受虚拟机中用户程序的影响,Xen利用了80x86架构中可用的四个保护级别。Xen VMM在最高特权级别(0)运行,来宾操作系统在下一个级别(1)运行,而应用程序在最低特权级别(3)运行。大多数针对80x86的操作系统将所有内容保持在特权级别0或3。
为了使子集功能正常工作,Xen修改了来宾操作系统,使其不使用架构中有问题的部分。例如,将Linux移植到Xen大约改动了3000行代码,约占80x86特定代码的1%。然而,这些更改并不影响来宾操作系统的应用程序二进制接口。
为了简化虚拟机的I/O挑战,Xen为每个硬件I/O设备分配了特权虚拟机。这些特殊的虚拟机称为驱动域(driver domains)。驱动域运行物理设备驱动程序,尽管中断仍由VMM处理,然后再发送到相应的驱动域。常规虚拟机称为来宾域(guest domains),运行简单的虚拟设备驱动程序,这些驱动程序必须通过通道与驱动域中的物理设备驱动程序进行通信,以访问物理I/O硬件。数据通过页面重映射在来宾域和驱动域之间传送。
2.5 跨越性问题:内存层次结构的设计  
本节描述了在其他章节中讨论的四个与内存层次结构密切相关的主题。
保护、虚拟化与指令集架构  
保护是架构与操作系统的共同努力,但随着虚拟内存的普及,架构师不得不修改一些现有指令集架构中的不便细节。例如,为了支持IBM 370中的虚拟内存,架构师必须对仅在6年前发布的成功的IBM 360指令集架构进行更改。今天,为了适应虚拟机,也正在进行类似的调整。
例如,80x86指令POPF从内存堆栈顶部加载标志寄存器。其中一个标志是中断使能(IE)标志。在最近为支持虚拟化而进行的更改之前,在用户模式下运行POPF指令,而不是将其捕获,简单地会改变所有标志,除了IE。在系统模式下,它确实会改变IE标志。由于来宾操作系统在虚拟机内的用户模式下运行,这成了一个问题,因为操作系统会期望看到IE的变化。对80x86架构的扩展以支持虚拟化解决了这个问题。
历史上,IBM大型机硬件和虚拟机监视器(VMM)采取了三个步骤来提高虚拟机的性能:
1. 降低处理器虚拟化的成本。
2. 减少由于虚拟化带来的中断开销。
3. 通过将中断导向适当的虚拟机而不调用VMM来降低中断成本。
IBM仍然是虚拟机技术的黄金标准。例如,2000年,IBM大型机同时运行了数千个Linux虚拟机,而Xen在2004年只能运行25个虚拟机(Clark等,2004)。最近版本的英特尔和AMD芯片组增加了特定指令,以支持在虚拟机中掩盖来自每个虚拟机的较低级别的中断,并将中断导向适当的虚拟机。
### 自主指令获取单元
许多具有乱序执行的处理器,甚至一些仅有深流水线的处理器,都将指令获取(有时还包括初始解码)解耦,使用单独的指令获取单元(见第3章)。通常,指令获取单元会访问指令缓存,以获取整个块,然后将其解码为单个指令;这种技术在指令长度变化时尤为有用。由于指令缓存是按块访问的,因此将未命中率与逐条访问指令缓存的处理器进行比较就变得没有意义。此外,指令获取单元还可以预取块到L1缓存;这些预取可能会生成额外的未命中,但实际上可能会减少总的未命中惩罚。许多处理器还包括数据预取,这可能会增加数据缓存的未命中率,同时降低总的数据缓存未命中惩罚。
### 推测与内存访问
高级流水线中使用的主要技术之一是推测,即在处理器确定指令是否真正需要之前,暂时执行该指令。这种技术依赖于分支预测,如果预测错误,则需要将推测的指令从流水线中清除。在支持推测的内存系统中,有两个独立的问题:保护和性能。
通过推测,处理器可能会生成内存引用,这些引用将永远不会被使用,因为这些指令是由于错误推测而产生的。如果执行这些引用,可能会产生保护异常。显然,这种错误只应在指令实际上被执行时发生。在下一章中,我们将看到如何解决这种“推测异常”。由于推测处理器可能会对指令和数据缓存进行访问,并随后不使用这些访问的结果,因此推测可能会增加缓存未命中率。然而,与预取一样,这种推测实际上可能会降低总的缓存未命中惩罚。推测的使用,如同预取的使用,使得将未命中率与没有推测的处理器进行比较变得具有误导性,即便它们的指令集架构(ISA)和缓存结构在其他方面是相同的。
### 特殊指令缓存
超标量处理器面临的最大挑战之一是提供足够的指令带宽。对于将指令转换为微操作的设计,例如最近的Arm和i7处理器,可以通过保持一个小型的最近翻译指令缓存来减少指令带宽需求和分支误预测惩罚。我们将在下一章中更深入地探讨这一技术。
### 缓存数据的一致性
数据可以存储在内存和缓存中。只要处理器是唯一更改或读取数据的组件,并且缓存位于处理器与内存之间,处理器看到旧的或过期的副本的风险就很小。然而,正如我们将看到的,多个处理器和I/O设备的存在增加了副本不一致的可能性,并可能导致读取错误的副本。
在多处理器系统中,缓存一致性问题的频率与I/O不同。对于I/O来说,多个数据副本是一个罕见事件——应尽量避免——但在多个处理器上运行的程序通常希望在多个缓存中保留相同数据的副本。多处理器程序的性能依赖于系统共享数据时的表现。
I/O缓存一致性的问题在于:I/O发生在计算机的哪个位置——在I/O设备与缓存之间,还是在I/O设备与主内存之间?如果输入将数据放入缓存,而输出从缓存读取数据,则I/O和处理器都看到相同的数据。这种方法的困难在于,它会干扰处理器,并可能导致处理器因I/O而停滞。输入也可能通过用不太可能很快被访问的新数据替换一些信息而干扰缓存。
在具有缓存的计算机中,I/O系统的目标是防止过期数据问题,同时尽量减少干扰。因此,许多系统倾向于直接对主内存进行I/O,使主内存充当I/O缓冲区。如果使用写透缓存,那么内存将拥有最新的信息副本,从而避免输出时出现过期数据的问题。(这一好处也是处理器倾向使用写透的原因。)然而,如今写透通常只出现在由使用写回的L2缓存支持的一级数据缓存中。
输入需要额外的工作。软件解决方案是确保输入缓冲区的任何块不在缓存中。包含缓冲区的页面可以标记为不可缓存,操作系统可以始终向该页面输入数据。或者,操作系统可以在输入发生之前从缓存中刷新缓冲区地址。硬件解决方案是在输入时检查I/O地址是否在缓存中。如果在缓存中发现匹配的I/O地址,则使缓存条目失效以避免过期数据。所有这些方法也可以用于使用写回缓存的输出。
在多核处理器时代,处理器缓存一致性是一个关键主题,我们将在第五章中详细探讨。
2.6 整合内容:ARM Cortex-A53 和 Intel Core i7 6700 的内存层次结构
本节揭示了 ARM Cortex-A53(以下简称 A53)和 Intel Core i7 6700(以下简称 i7)的内存层次结构,并展示了它们各自组件在一组单线程基准测试中的性能。我们首先研究 Cortex-A53,因为它具有更简单的内存系统;然后更详细地探讨 i7,详细追踪一次内存引用。本节假设读者熟悉使用虚拟索引缓存的两级缓存层次结构的组织。关于这种内存系统的基本知识将在附录 B 中详细解释,建议对该系统组织不确定的读者仔细阅读附录 B 中的 Opteron 示例。一旦他们理解了 Opteron 的组织,A53 系统的简要说明(与其相似)将容易理解。
### ARM Cortex-A53
Cortex-A53 是一个可配置的核心,支持 ARMv8A 指令集架构,包括 32 位和 64 位模式。Cortex-A53 作为 IP(知识产权)核心提供。IP 核心是嵌入式、PMD 和相关市场中技术交付的主流形式;数十亿个 ARM 和 MIPS 处理器都是由这些 IP 核心构建而成。需要注意的是,IP 核心与 Intel i7 或 AMD Athlon 多核处理器中的核心不同。IP 核心(它本身也可以是多核)设计为与其他逻辑结合使用(因此它是芯片的核心),包括应用特定处理器(如视频编码器或解码器)、I/O 接口和内存接口,然后制造出针对特定应用优化的处理器。例如,Cortex-A53 IP 核心被广泛应用于各种平板电脑和智能手机;它被设计为高能效,这是电池驱动 PMD 的关键标准。A53 核心能够根据需求配置多个核心以用于高端 PMD;我们在这里的讨论集中在单个核心上。
一般来说,IP 核心有两种类型。硬核(Hard cores)针对特定半导体厂商进行了优化,通常是黑箱,具有外部(但仍在芯片内)接口。硬核通常只允许对核心外部的逻辑进行参数化,比如 L2 缓存大小,而 IP 核心本身不能被修改。软核(Soft cores)通常以使用标准逻辑元件库的形式提供。软核可以为不同的半导体厂商编译,并且也可以进行修改,尽管由于现代 IP 核心的复杂性,进行大规模修改非常困难。一般而言,硬核提供更高的性能和更小的芯片面积,而软核则允许重新定位到其他厂商并且更容易修改。
Cortex-A53 可以在高达 1.3 GHz 的时钟频率下每个时钟周期发出两条指令。它支持两级 TLB 和两级缓存;图 2.19 总结了内存层次结构的组织。关键术语首先被返回,处理器可以在缺失完成时继续执行;支持最多四个银行的内存系统。对于 32 KiB 的 D-cache 和 4 KiB 的页面大小,每个物理页面可以映射到两个不同的缓存地址;通过硬件检测在缺失情况下避免这种别名,如附录 B 的 B.3 节所述。图 2.20 显示了如何使用 32 位虚拟地址来索引 TLB 和缓存,假设有 32 KiB 的主缓存和 1 MiB 的二级缓存,页面大小为 16 KiB。

图 2.19 Cortex A53 的内存层次结构包括多级 TLB 和缓存。页面映射缓存跟踪一组虚拟页面的物理页面位置;它减少了 L2 TLB 缺失惩罚。L1 缓存是虚拟索引和物理标记的;L1 数据缓存和 L2 都采用写回策略,默认在写入时进行分配。所有缓存中的替换策略都是 LRU 近似。如果同时发生 MicroTLB 和 L1 缺失,L2 的缺失惩罚会更高。L2 到主内存的总线宽度为 64–128 位,窄总线的缺失惩罚更大。

图 2.20 假设使用 32 位地址,展示了 ARM Cortex-A53 的虚拟地址、物理地址和数据块。上半部分 (A) 显示指令访问;下半部分 (B) 显示数据访问,包括 L2。TLB(指令或数据)是完全关联的,每个都有 10 个条目,在这个例子中使用 64 KiB 的页面。L1 指令缓存是二路组相联,具有 64 字节块和 32 KiB 的容量;L1 数据缓存为 32 KiB,四路组相联,块大小为 64 字节。L2 TLB 有 512 个条目,并且是四路组相联。L2 缓存是十六路组相联,块大小为 64 字节,容量从 128 KiB 到 2 MiB;这里展示的是 1 MiB 的 L2。这张图没有显示缓存和 TLB 的有效位和保护位。
#### Cortex-A53 内存层次的性能
Cortex-A8 的内存层次在运行 SPECInt2006 基准测试时,使用了 32 KiB 的主缓存和 1 MiB 的 L2 缓存。对于这些 SPECInt2006 测试,指令缓存的缺失率非常小,即使仅考虑 L1:大多数情况下接近零,所有情况下均低于 1%。这一低缺失率可能是由于 SPECCPU 程序的计算密集型特性以及二路组相联缓存有效消除了大部分冲突缺失。
图 2.21 显示了数据缓存的结果,其 L1 和 L2 的缺失率显著。L1 的缺失率变化范围大约为 75 倍,从 0.5% 到 37.3%,中位缺失率为 2.4%。全局 L2 的缺失率变化范围为 180 倍,从 0.05% 到 9.0%,中位数为 0.3%。MCF 被称为缓存破坏者,设置了上限并显著影响了平均值。需要注意的是,L2 全局缺失率明显低于 L2 局部缺失率;例如,中位 L2 单独缺失率为 15.1%,而全局缺失率为 0.3%。

图 2.21 显示了使用 SPECInt2006 基准测试时,ARM 的 32 KiB L1 缓存的数据缺失率和 1 MiB L2 缓存的全局数据缺失率受应用程序的显著影响。具有较大内存占用的应用程序在 L1 和 L2 中往往会有更高的缺失率。请注意,L2 缺失率是全局缺失率,计算了所有引用,包括那些在 L1 中命中的引用。MCF 被称为缓存破坏者。
使用图 2.19 中的缺失惩罚,图 2.22 显示了每次数据访问的平均惩罚。尽管 L1 的缺失率约为 L2 的七倍,但 L2 的惩罚却高出 9.5 倍,这导致 L2 的缺失在压力测试内存系统的基准测试中略占优势。在下一章中,我们将探讨缓存缺失对整体 CPI 的影响。

图 2.22 显示了 A53 处理器在运行 SPECInt2006 时,每次数据存储引用的平均内存访问惩罚,包括来自 L1 和 L2 的惩罚。尽管 L1 的缺失率明显更高,但 L2 的缺失惩罚却高出五倍以上,这意味着 L2 的缺失会显著影响性能。
**Intel Core i7 6700**
i7 支持 x86-64 指令集架构,这是 80x86 架构的 64 位扩展。i7 是一款乱序执行处理器,包含四个核心。本章重点从单核的角度讨论内存系统设计和性能。多处理器设计的系统性能,包括 i7 多核处理器,会在第五章中详细探讨。
i7 的每个核心每个时钟周期可以执行最多四条 80x86 指令,使用一种多发射、动态调度的 16 阶段流水线,我们将在第三章中详细描述。i7 还支持每个处理器最多两个同时线程,采用称为同时多线程技术,详见第四章。2017 年,最快的 i7 的时钟频率为 4.0 GHz(在 Turbo Boost 模式下),这使得其峰值指令执行率达到每秒 160 亿条指令,四核设计则为每秒 640 亿条指令。当然,峰值性能和持续性能之间存在较大差距,接下来的几章将对此进行分析。
i7 可以支持最多三个内存通道,每个通道由一组独立的 DIMM 组成,并且可以并行传输。使用 DDR3-1066(DIMM PC8500),i7 的峰值内存带宽超过 25 GB/s。i7 使用 48 位虚拟地址和 36 位物理地址,最大物理内存为 36 GiB。内存管理采用两级 TLB(见附录 B,第 B.4 节),如图 2.23 所示。

**图 2.23** i7 的 TLB 结构特征,其中包含独立的一级指令和数据 TLB,二者均由联合的二级 TLB 支持。一级 TLB 支持标准的 4 KiB 页大小,同时也有限地支持 2–4 MiB 大页;在二级 TLB 中仅支持 4 KiB 页。i7 能够并行处理两个 L2 TLB 未命中。有关多级 TLB 和对多种页大小支持的更多讨论,请参见在线附录 L 的第 L.3 节。
图 2.24 总结了 i7 的三级缓存层次结构。一级缓存是虚拟索引和物理标记的(见附录 B,第 B.3 节),而 L2 和 L3 缓存是物理索引的。某些版本的 i7 6700 将支持使用 HBM 封装的四级缓存。

**图 2.24** i7 中三级缓存层次结构的特征。所有三个缓存均使用写回策略和 64 字节的块大小。L1 和 L2 缓存对于每个核心是独立的,而 L3 缓存则在芯片上的各个核心之间共享,且每个核心的总容量为 2 MiB。所有三个缓存都是非阻塞的,允许多个未完成的写操作。L1 缓存使用合并写缓冲区,以便在写入时如果该行不在 L1 中,则保存数据。(也就是说,L1 写未命中不会导致该行被分配。)L3 是 L1 和 L2 的包含;我们在解释多处理器缓存时将进一步探讨这一特性。替换策略采用一种伪 LRU 的变体;在 L3 的情况下,所替换的块始终是访问位为关闭状态的最低编号方式。这并不是完全随机,但计算起来相对简单。
图 2.25 标明了访问内存层次结构的步骤。首先,程序计数器(PC)被发送到指令缓存。指令缓存的索引是


或者 6 位。指令地址的页面框架(36¼48% 12 位)被发送到指令 TLB(步骤 1)。同时,来自虚拟地址的 12 位页面偏移被发送到指令缓存(步骤 2)。注意,对于八路组相联的指令缓存,需要 12 位作为缓存地址:6 位用于索引缓存,加上 6 位用于 64 字节块的块偏移,因此不会出现别名。之前版本的 i7 使用四路组相联的 I-cache,这意味着与虚拟地址对应的块实际上可以在缓存中的两个不同位置,因为相应的物理地址在该位置可以是 0 或 1。对于指令而言,这并不构成问题,因为即使一条指令在缓存中出现在两个不同的位置,这两个版本也必须是相同的。如果允许数据的这种重复或别名,页面映射更改时必须检查缓存,而这种情况并不频繁。请注意,非常简单的页面着色方法(见附录 B,第 B.3 节)可以消除这些别名的可能性。如果偶数地址的虚拟页面映射到偶数地址的物理页面(奇数页面也是如此),那么这些别名就不会发生,因为虚拟和物理页面号中的低位将是相同的。
然后访问指令 TLB,以找到地址与有效页面表项(PTE)之间的匹配(步骤 3 和 4)。除了转换地址,TLB 还会检查 PTE 是否要求此访问因访问违规而导致异常。
指令 TLB 缺失首先会转到 L2 TLB,它包含 1536 个 4 KiB 页面大小的页面表项(PTE),并且是 12 路组相联的。从 L2 TLB 加载数据到 L1 TLB 需要 8 个时钟周期,这导致包括访问 L1 TLB 的初始时钟周期在内的 9 个周期缺失惩罚。如果 L2 TLB 也缺失,则使用硬件算法遍历页面表并更新 TLB 条目。在线附录 L 的 L.5 和 L.6 节描述了页面表遍历器和页面结构缓存。在最坏的情况下,页面不在内存中,操作系统将从二级存储中获取该页面。由于在页面错误期间可能会执行数百万条指令,如果有其他进程等待运行,操作系统将会交换进程。否则,如果没有 TLB 异常,指令缓存访问将继续进行。
地址的索引字段被发送到指令缓存的所有八个银行(步骤 5)。指令缓存标签为 36 位 = 6 位(索引)+ 6 位(块偏移),即 24 位。四个标签和有效位与来自指令 TLB 的物理页框进行比较(步骤 6)。由于 i7 每次取指期望 16 字节,因此从 6 位块偏移中额外使用 2 位来选择适当的 16 字节。因此,6 + 2 或 8 位用于将 16 字节的指令发送给处理器。L1 缓存是管道化的,命中延迟为 4 个时钟周期(步骤 7)。如果未命中,则转向二级缓存。
如前所述,指令缓存是虚拟寻址的,而物理标记的。由于二级缓存是物理寻址的,因此来自 TLB 的物理页面地址与页面偏移组合以生成访问 L2 缓存的地址。L2 索引是

因此,30 位块地址(36 位物理地址 % 6 位块偏移)被分为 20 位标签和 10 位索引(步骤 8)。再次,索引和标签被发送到统一的 L2 缓存的四个银行(步骤 9),并行比较。如果其中一个匹配且有效(步骤 10),它将在初始的 12 个周期延迟后以每个时钟周期 8 字节的速率按顺序返回块。
如果 L2 缓存未命中,则访问 L3 缓存。对于具有 8 MiB L3 的四核 i7,索引大小是

将13位索引(步骤11)发送到所有16个L3缓存银行(步骤12)。L3标签(36% (13 + 6) ≈ 17位)与来自TLB的物理地址进行比较(步骤13)。如果命中,数据块将在初始延迟42个时钟周期后返回,以每个时钟16字节的速率放入L1和L3。如果L3未命中,则会发起内存访问。
如果在L3缓存中未找到指令,片上内存控制器必须从主内存中获取数据块。i7具有三个64位内存通道,可以作为一个192位通道使用,因为只有一个内存控制器,并且相同的地址会通过两个通道发送(步骤14)。当两个通道具有相同的DIMM时,会发生宽幅传输。每个通道最多支持四个DDR DIMM(步骤15)。当数据返回时,它们会被放入L3和L1中(步骤16),因为L3是包含的。
由主内存服务的指令未命中的总延迟大约为42个处理器周期,以确定发生了L3未命中,加上关键指令的DRAM延迟。对于单银行的DDR4-2400 SDRAM和4.0 GHz的CPU,DRAM延迟约为40纳秒或160个时钟周期到前16字节,总未命中惩罚约为200个时钟周期。内存控制器以每个I/O总线时钟周期16字节的速率填充剩余的64字节缓存块,这需要额外5纳秒或20个时钟周期。
由于二级缓存是写回缓存,任何未命中都可能导致旧数据块被写回内存。i7具有10条项的合并写缓冲区,当下一级缓存未被读取时写回脏缓存行。在未命中时会检查写缓冲区以查看缓存行是否存在于缓冲区中;如果存在,未命中将从缓冲区填充。L1和L2缓存之间也使用类似的缓冲区。如果这个初始指令是加载,数据地址会被发送到数据缓存和数据TLB,就像访问指令缓存一样。
假设指令是存储而不是加载。当发出存储时,它会像加载一样进行数据缓存查找。未命中时,数据块会被放入写缓冲区,因为L1缓存在写未命中时不会分配该块。在命中时,存储不会立即更新L1(或L2)缓存,直到知道其不是推测性的。在此期间,存储保持在负载-存储队列中,这是处理器乱序控制机制的一部分。
i7还支持从层次结构的下一级对L1和L2进行预取。在大多数情况下,预取的行只是缓存中的下一个块。通过仅对L1和L2进行预取,可以避免高成本的不必要的内存抓取。
### i7内存系统性能
我们使用SPECint2006基准测试评估i7缓存结构的性能。本节的数据由路易斯安那州立大学的彭卢教授和博士生刘群收集。他们的分析基于之前的工作(见Prakash和Peng,2008年)。
i7管道的复杂性,包括自主指令抓取单元、推测执行,以及指令和数据预取,使得与更简单的处理器比较缓存性能变得困难。如第110页所提到的,使用预取的处理器可以生成与程序执行的内存访问无关的缓存访问。由于实际的指令访问或数据访问而生成的缓存访问有时被称为需求访问,以区别于预取访问。需求访问可以来自推测性的指令抓取和推测性的数据访问,其中一些随后会被取消(有关推测和指令毕业的详细描述,请参见第3章)。推测处理器生成的未命中数量至少与顺序非推测处理器相同,通常还会更多。除了需求未命中外,还有指令和数据的预取未命中。
i7的指令抓取单元试图每个周期获取16字节,这使得比较指令缓存未命中率变得复杂,因为每个周期都会抓取多个指令(平均约4.5条)。实际上,整个64字节的缓存行会被读取,而后续的16字节抓取不需要额外的访问。因此,仅根据64字节块跟踪未命中情况。32 KiB的八路组相联指令缓存使得SPECint2006程序的指令未命中率非常低。如果为了简化,我们将SPECint2006的未命中率定义为64字节块的未命中次数除以完成的指令数,则所有基准的未命中率都低于1%,只有一个基准(XALANCBMK)具有2.9%的未命中率。由于一个64字节块通常包含16到20条指令,因此每条指令的有效未命中率要低得多,这取决于指令流中的空间局部性程度。
指令获取单元因等待I-cache未命中而停滞的频率同样很小(占总周期的百分比),对于两个基准测试增加到2%,而XALANCBMK的I-cache未命中率最高,达到了12%。在下一章中,我们将看到IFU中的停滞如何影响i7的整体管道吞吐量。
L1数据缓存则更有趣,也更难以评估,因为除了预取和推测的影响外,L1数据缓存并不是写分配的,对不存在的缓存块的写入不会视为未命中。因此,我们只关注内存读取。i7的性能监控测量将预取访问与需求访问区分开,但仅保留那些成功完成的指令的需求访问。未成功完成的推测指令的影响不可忽视,尽管管道效应可能主导由推测引起的二级缓存效应;我们将在下一章回到这个问题。

图2.26展示了SPECint2006基准测试中L1数据缓存的未命中率,有两种表示方式:一种是包括需求读取和预取访问,另一种仅包含需求访问。i7将不在缓存中的块的L1未命中与已经在处理并从L2进行预取的块的L1未命中分开;我们将后者视为命中,因为它们在阻塞缓存中会命中。这些数据和本节其余部分的数据均由路易斯安那州立大学的彭卢教授和博士生刘群收集,基于对Intel Core Duo及其他处理器的早期研究(见Peng et al., 2008)。
为了解决这些问题,同时保持数据量的合理性,图2.26以两种方式展示了L1数据缓存的未命中情况:
1. 包括预取和推测加载的L1未命中率相对于需求引用,计算公式为:L1未命中率(包括预取和推测加载)/ L1需求读取引用,针对那些成功完成的指令。
2. 仅考虑需求未命中的未命中率,计算公式为:L1需求未命中/L1需求读取引用,同样只针对成功完成的指令。
平均而言,包括预取的未命中率是仅考虑需求的未命中率的2.8倍。将这些数据与早期的i7 920进行比较,后者具有相同大小的L1缓存,我们发现新款i7的包括预取的未命中率更高,但导致停滞的需求未命中数通常较少。
为了理解i7中激进的预取机制的有效性,让我们看看一些关于预取的测量数据。图2.27展示了L2请求中预取与需求请求的比例以及预取未命中率。乍一看,这些数据可能令人震惊:预取请求大约是L2需求请求的1.5倍,而这些需求请求直接来自L1未命中。此外,预取未命中率非常高,平均未命中率为58%。尽管预取比例差异很大,但预取未命中率始终显著。乍一看,你可能会得出设计者犯了错误的结论:他们预取太多,未命中率过高。然而,注意到高预取比例的基准测试(如ASTAR、BZIP2、HMMER、LIBQUANTUM和OMNETPP)也显示出预取未命中率和需求未命中率之间的差距最大,在每种情况下都超过2倍。激进的预取机制是在用提前发生的预取未命中换取稍后发生的需求未命中;因此,由于预取,管道停顿的可能性降低。

图2.27显示了L2请求中预取请求的比例,柱状图和左侧轴表示这一比例。右侧轴和折线则展示了预取命中率。这些数据与本节其余部分的数据一样,是由路易斯安那州立大学的卢鹏教授和博士生刘群基于对英特尔Core Duo及其他处理器的早期研究收集的(参见Peng et al., 2008)。
同样,考虑到高预取未命中率。假设大多数预取实际上是有用的(这很难测量,因为需要跟踪单个缓存块),那么预取未命中意味着未来可能会出现L2缓存未命中。通过预取提前发现并处理未命中,很可能会减少停顿周期。对像i7这样的推测超标量处理器进行的性能分析表明,缓存未命中往往是管道停顿的主要原因,因为保持处理器运行尤其困难,特别是在L2和L3未命中持续较长时间时。英特尔设计师无法轻易增加缓存的大小而不影响能耗和周期时间;因此,使用激进的预取来尝试降低有效缓存未命中的惩罚,是一种有趣的替代方法。
结合L1需求未命中和预取请求,大约17%的加载操作生成L2请求。分析L2性能需要考虑写入的影响(因为L2是写分配的),以及预取命中率和需求命中率。图2.28展示了L2缓存对于需求和预取访问的未命中率,分别与L1引用次数(读取和写入)进行比较。与L1一样,预取是一个重要的贡献者,产生了75%的L2未命中。将L2需求未命中率与早期的i7实现(同样大小的L2)进行比较显示,i7 6700的L2需求未命中率大约低了2倍,这很可能证明了更高的预取未命中率是合理的。

图2.28显示了L2需求未命中率和预取未命中率,这两者都是相对于所有对L1的访问而言,其中也包括预取、未完成的推测加载以及程序生成的加载和存储(需求引用)。这些数据与本节其余部分的数据一样,是由路易斯安那州立大学的卢鹏教授和博士生刘群收集的。
由于内存未命中的代价超过100个周期,而L2的平均数据未命中率(结合预取和需求未命中)超过7%,因此L3显得尤为关键。如果没有L3,并假设大约三分之一的指令是加载或存储,那么L2缓存未命中可能会使每条指令的CPI增加超过两个周期!显然,如果没有L3,L2的预取就毫无意义。
相比之下,平均L3数据未命中率为0.5%,虽然仍然显著,但不到L2需求未命中率的三分之一,并且比L1需求未命中率低10倍。只有在两个基准测试(OMNETPP和MCF)中,L3未命中率超过0.5%;在这两种情况下,约2.3%的未命中率可能主导了所有其他性能损失。在下一章中,我们将研究i7的CPI与缓存未命中之间的关系,以及其他管道效应。
2.7 Fallacies and Pitfalls
作为计算机体系结构学科中最具数量化特征的领域,内存层次结构似乎不太容易受到谬论和陷阱的影响。然而,我们在这里受限的并不是缺乏警告,而是缺乏空间!
**谬论:从一个程序预测另一个程序的缓存性能。** 图2.29展示了来自SPEC2000基准套件的三个程序在缓存大小变化时的指令未命中率和数据未命中率。根据程序的不同,4096 KiB缓存下每千条指令的数据未命中率分别为9、2或90,而4 KiB缓存下每千条指令的指令未命中率则为55、19或0.0004。商业程序如数据库即使在大型二级缓存中也会有显著的未命中率,这通常不是SPECCPU程序的情况。显然,从一个程序推广缓存性能到另一个程序是不可取的。正如图2.24所提醒我们的那样,存在很大的变化,甚至关于整数和浮点密集型程序相对未命中率的预测也可能是错误的,正如mcf和sphinx3所提醒我们的那样!

图2.29 显示了缓存大小从4 KiB变化到4096 KiB时,每千条指令的指令未命中和数据未命中率。gcc的指令未命中率是lucas的30,000到40,000倍,而反过来,lucas的数据未命中率是gcc的2到60倍。程序gap、gcc和lucas均来自SPEC2000基准套件。
**陷阱:模拟足够的指令以获取内存层次结构的准确性能测量。**
这里实际上有三个陷阱。第一个是试图使用小的跟踪数据来预测大型缓存的性能。第二个是程序的局部性行为在整个运行过程中并不是恒定的。第三个是程序的局部性行为可能会根据输入的不同而有所变化。
图2.30显示了对单个SPEC2000程序五个输入的每千条指令的累积平均指令未命中率。对于这些输入,前19亿条指令的平均内存未命中率与其余执行过程中的平均未命中率非常不同。

图2.30 显示了对SPEC2000中perl基准的五个输入,每千次引用的指令未命中率。在前19亿条指令中,未命中率变化很小,五个输入之间几乎没有差别。完整运行显示了未命中率在程序生命周期中的变化以及它们如何依赖于输入。顶部图表显示了前19亿条指令的运行平均未命中率,起始约为2.5,结束时所有五个输入的未命中率约为4.7每千次引用。底部图表显示了完整运行的运行平均未命中率,这个过程根据输入需要16到41亿条指令。在前19亿条指令之后,每千次引用的未命中率根据输入变化在2.4到7.9之间。模拟是针对Alpha处理器进行的,使用了分别用于指令和数据的独立L1缓存,每个缓存为双路64 KiB,采用LRU替换策略,并配备统一的1 MiB直接映射L2缓存。
**陷阱**:在基于缓存的系统中未能提供高内存带宽。  
缓存有助于减少平均缓存内存延迟,但可能无法为必须访问主内存的应用程序提供高内存带宽。架构师必须设计一个高带宽内存,以便在缓存后面支持此类应用程序。我们将在第4章和第5章重温这个陷阱。
**陷阱**:在未设计为虚拟化的指令集架构上实现虚拟机监控器。  
许多架构师在1970年代和1980年代并没有仔细确保所有读取或写入与硬件资源信息相关的信息的指令都是特权指令。这种放任自流的态度给所有这些架构的虚拟机监控器(VMM)带来了问题,包括我们在这里作为示例使用的80x86架构。图2.31描述了导致半虚拟化问题的18条指令(Robin和Irvine,2000)。这两大类指令是:
- 在用户模式下读取控制寄存器,显示客操作系统正在虚拟机中运行(例如,前面提到的POPF)。
- 根据分段架构的要求检查保护,但假设操作系统以最高特权级别运行。
虚拟内存也是一个挑战。由于80x86 TLB不支持进程ID标签,而大多数RISC架构支持,因此VMM和客操作系统共享TLB的成本更高;每次地址空间更改通常需要刷新TLB。

**图2.31**:导致虚拟化问题的18条80x86指令总结(Robin和Irvine,2000)。  
顶部组的前五条指令允许用户模式下的程序读取控制寄存器,例如描述符表寄存器,而不会引发异常。POP FLAGS指令修改了包含敏感信息的控制寄存器,但在用户模式下默默失败。80x86分段架构的保护检查是底部组的致命缺陷,因为这些指令在读取控制寄存器时会隐式检查特权级别。该检查假设操作系统必须处于最高特权级别,但这对于客户虚拟机并不成立。只有MOVE到段寄存器尝试修改控制状态,但保护检查也阻止了这一操作。
虚拟化I/O对于80x86架构也是一个挑战,部分原因在于它支持内存映射I/O并且有独立的I/O指令,但更重要的是,PC设备和设备驱动程序的种类和数量非常庞大,导致虚拟机监控器(VMM)需要处理这些复杂性。第三方供应商提供自己的驱动程序,而这些驱动程序可能未能正确虚拟化。传统的虚拟机实现的一种解决方案是将真实设备驱动程序直接加载到VMM中。
为了简化在80x86上的虚拟机监控器实现,AMD和Intel都提出了对架构的扩展。Intel的VT-x提供了一种新的执行模式来运行虚拟机,定义了虚拟机状态的架构化定义,提供了快速切换虚拟机的指令,以及一套参数来选择何时必须调用VMM。总的来说,VT-x为80x86添加了11条新指令。AMD的安全虚拟机(SVM)提供了类似的功能。
在启用VT-x支持的模式后(通过新的VMXON指令),VT-x为客户操作系统提供了四个优先级低于原始四个的特权级别(并修复了之前提到的POPF指令问题)。VT-x捕获虚拟机的所有状态到虚拟机控制状态(VMCS)中,并提供原子指令来保存和恢复VMCS。除了关键状态外,VMCS还包含配置信息,用以确定何时调用VMM,以及具体是什么原因导致VMM被调用。为了减少VMM调用的次数,这种模式增加了某些敏感寄存器的影像版本,并添加了掩码,以检查在触发之前敏感寄存器的关键位是否会被改变。为了降低虚拟化虚拟内存的成本,AMD的SVM增加了一个额外的间接级别,称为嵌套页表,这使得影像页表变得不必要(请参见附录L的第L.7节)。
2.8 Concluding Remarks: Looking Ahead
在过去三十年里,有许多关于计算机性能提升即将停滞的预测。每一个这样的预测都是错误的。这些错误的原因在于,它们依赖于一些未明确说明的假设,而这些假设在随后的事件中被推翻。例如,未能预见从离散组件到集成电路的转变,导致了一个预测,即光速将限制计算机速度,使其慢几个数量级于现在的水平。我们对内存墙的预测可能也是错误的,但它表明我们需要开始“跳出框架”思考。
——Wm. A. Wulf 和 Sally A. McKee,《击中内存墙:显而易见的影响》,维吉尼亚大学计算机科学系(1994年12月)。本文首次引入了“内存墙”这一术语。
使用内存层次结构的可能性可以追溯到20世纪40年代末和50年代初通用数字计算机的早期阶段。虚拟内存在60年代初期被引入研究计算机中,并在70年代进入IBM大型机。缓存大约在同一时期出现。这些基本概念随着时间的推移得到了扩展和增强,以帮助缩小主内存与处理器之间的访问时间差距,但基本概念依然保持不变。
一个导致内存层次结构设计发生重大变化的趋势是DRAM的密度和访问时间持续放缓。在过去的15年中,这两种趋势都得到了观察,并在过去5年中变得更加明显。尽管DRAM带宽有所增加,但访问时间的减少则进展缓慢,几乎在DDR4和DDR3之间消失。Dennard缩放的结束以及摩尔定律的放缓都促成了这种情况。DRAM中使用的埋入电容设计也限制了其扩展能力。可能的情况是,诸如堆叠内存等封装技术将成为改善DRAM访问带宽和延迟的主要来源。
独立于DRAM的改进,Flash内存发挥了更大的作用。在PMD(便携式媒体设备)中,Flash已经主导了15年,并且在近10年前成为笔记本电脑的标准配置。在过去几年中,许多台式机也以Flash作为主要的二级存储。Flash相较于DRAM的潜在优势在于没有每比特控制写入的晶体管,但这也是其致命弱点。Flash必须使用批量擦除-重写周期,这样的速度明显较慢。因此,尽管Flash已成为增长最快的二级存储形式,SDRAM仍然在主存储中占据主导地位。
虽然作为内存基础的相变材料存在已久,但它们从未成为磁盘或Flash的真正竞争对手。英特尔和美光最近宣布的交叉点技术可能会改变这一局面。这项技术似乎在多个方面优于Flash,包括消除了缓慢的擦除-写入周期以及更长的使用寿命。这项技术有可能最终取代主导大容量存储超过50年的电机械磁盘!
多年来,人们对即将到来的内存墙做出过各种预测(参见之前引用的论文),这将严重限制处理器性能。幸运的是,多级缓存的扩展(从2级到4级)、更复杂的填充和预取方案、编译器和程序员对局部性重要性的更高意识,以及DRAM带宽的巨大改善(自1990年代中期以来提高了超过150倍)帮助我们抵御了内存墙的威胁。近年来,L1缓存的访问时间限制(受时钟周期限制)和L2、L3缓存的能量相关限制带来了新的挑战。i7处理器系列在6到7年间的发展便体现了这一点:i7 6700中的缓存大小与第一代i7处理器相同!更积极地使用预取是一种试图克服无法增加L2和L3缓存大小的解决方案。由于离芯片L4缓存的能量限制较小,它们可能变得愈发重要。
除了依赖多级缓存的方案,采用具有多个未完成缺失的乱序管线的引入,使得可用的指令级并行性能够隐藏基于缓存系统中剩余的内存延迟。引入多线程和更多线程级并行性则进一步提升了这一点,提供了更多的并行性,从而带来更多隐藏延迟的机会。在现代多级缓存系统中,使用指令级和线程级并行性可能将成为隐藏遇到的各种内存延迟的重要工具。
一个周期性出现的想法是使用程序员控制的临时存储器或其他高速度可见内存,这在 GPU 中有所应用。然而,由于几个原因,这种想法从未在通用处理器中成为主流:首先,它们通过引入具有不同行为的地址空间来打破内存模型。其次,与基于编译器或程序员的缓存优化(例如预取)不同,使用临时存储器的内存转换必须完全处理从主内存地址空间到临时存储器地址空间的重新映射。这使得这种转换更加困难,适用性也受到限制。在 GPU 中(见第4章),局部临时存储器被广泛使用,管理这些存储器的负担目前落在了程序员身上。对于能够使用这些存储器的特定领域软件系统,性能提升非常显著。因此,HBM 技术很可能会被用于大型通用计算机中的缓存,甚至很可能作为图形和类似系统中的主要工作内存。随着特定领域架构在克服 Dennard 定律终结和摩尔定律减缓所带来的限制中变得愈加重要,临时存储器和类向量寄存器集的使用可能会增加。
Dennard 定律的终结对 DRAM 和处理器技术都有影响。因此,我们可能不会看到处理器和主内存之间的鸿沟扩大,而是两种技术的放缓,将导致整体性能增长率的减慢。计算机架构及相关软件的新创新,将共同提高性能和效率,是继续实现过去50年所见的性能提升的关键。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值