目录
[INTRODUCTION TO THE SECOND EDITION](#INTRODUCTION TO THE SECOND EDITION)
[INTRODUCTION TO THE FIRST EDITION](#INTRODUCTION TO THE FIRST EDITION)
[Chapter1 WHAT IS A CACHE MEMORY?](#Chapter1 WHAT IS A CACHE MEMORY?)
[1.1 CPU SPEED VS. SYSTEM SPEED](#1.1 CPU SPEED VS. SYSTEM SPEED)
[1.2 THE COST OF FAST MAIN MEMORY](#1.2 THE COST OF FAST MAIN MEMORY)
[1.3 THE CONCEPT OF LOCALITY](#1.3 THE CONCEPT OF LOCALITY)
[1.4 FOOLING THE CPU](#1.4 FOOLING THE CPU)
[1.5 CACHE DATA AND CACHE-TAG MEMORIES](#1.5 CACHE DATA AND CACHE-TAG MEMORIES)
[1.6 THRASHING: GETTING THE MOST OUT OF THE CACHE](#1.6 THRASHING: GETTING THE MOST OUT OF THE CACHE)
[1.7 CACHES AND THE MEMORY HIERARCHY](#1.7 CACHES AND THE MEMORY HIERARCHY)
[1.8 REDUCING BUS TRAFFIC](#1.8 REDUCING BUS TRAFFIC)
[1.9 REDUCING POWER CONSUMPTION](#1.9 REDUCING POWER CONSUMPTION)
[1.10 AN EXAMPLE CACHE](#1.10 AN EXAMPLE CACHE)
[Chapter2 HOW ARE CACHES DESIGNED?](#Chapter2 HOW ARE CACHES DESIGNED?)
[2.1 THE CPU-TO-MAIN-MEMORY INTERFACE](#2.1 THE CPU-TO-MAIN-MEMORY INTERFACE)
[2.2 CHOOSING CACHE POLICIES](#2.2 CHOOSING CACHE POLICIES)
[2.3 STATISTICAL PREMISES](#2.3 STATISTICAL PREMISES)
[2.4 SOFTWARE PROBLEMS AND SOLUTIONS](#2.4 SOFTWARE PROBLEMS AND SOLUTIONS)
[2.5 REAL-WORLD PROBLEMS](#2.5 REAL-WORLD PROBLEMS)
[Chapter3 CACHE MEMORIES AND RISC PROCESSORS](#Chapter3 CACHE MEMORIES AND RISC PROCESSORS)
[3.1 THE RISC CONCEPT](#3.1 THE RISC CONCEPT)
[3.2 FEEDING INSTRUCTIONS TO A RISC CPU](#3.2 FEEDING INSTRUCTIONS TO A RISC CPU)
[3.3 PROBLEMS UNIQUE TO RISC CACHES](#3.3 PROBLEMS UNIQUE TO RISC CACHES)
[Chapter4 MAINTAINING COHERENCY IN CACHED SYSTEMS](#Chapter4 MAINTAINING COHERENCY IN CACHED SYSTEMS)
[4.1 SINGLE-PROCESSOR SYSTEMS](#4.1 SINGLE-PROCESSOR SYSTEMS)
[4.2 MULTIPLE-PROCESSOR SYSTEMS](#4.2 MULTIPLE-PROCESSOR SYSTEMS)
[4.3 EXISTING COPY-BACK COHERENCY PROTOCOLS](#4.3 EXISTING COPY-BACK COHERENCY PROTOCOLS)
[Chapter5 INTERESTING CACHE TRICKS](#Chapter5 INTERESTING CACHE TRICKS)
[5.1 EFFICIENTLY FEEDING A SUPERSCALAR MACHINE](#5.1 EFFICIENTLY FEEDING A SUPERSCALAR MACHINE)
[5.2 PRIMARY AND SECONDARY CACHES ON THE SAME CHIP](#5.2 PRIMARY AND SECONDARY CACHES ON THE SAME CHIP)
[5.3 CONTROLLING THE CACHE'S CONSISTENCY WITH ITS WRITE BUFFERS](#5.3 CONTROLLING THE CACHE'S CONSISTENCY WITH ITS WRITE BUFFERS)
[5.4 ACHIEVING A TRUE LRU BY USING A STACK](#5.4 ACHIEVING A TRUE LRU BY USING A STACK)
[5.5 A "SEMIASSOCIATIVE" CACHE](#5.5 A "SEMIASSOCIATIVE" CACHE)
[5.6 CONTROLLING ALIASES BY USING EIGHT COMPARATORS](#5.6 CONTROLLING ALIASES BY USING EIGHT COMPARATORS)
[5.7 WRITE BUFFERING VS. MULTIPROCESSING](#5.7 WRITE BUFFERING VS. MULTIPROCESSING)
INTRODUCTION TO THE SECOND EDITION
尽管第一版出版已经四年了,令人惊讶的是缓存设计的基础原理基本保持不变。尽管处理器芯片上的晶体管数量有了很大进展,其中大部分用于缓存存储器,但我们发现片上缓存使用的策略几乎没有变化。除了在处理写周期时进行了一些较小的改进外,结构相对来说仍然很简单。第一版中详细介绍的几乎所有内容现在都在使用。
那么,为什么需要第二版呢?无论书籍写得多么永恒,科技书籍也需要不断修订,这本书也不例外。原始文本中使用的许多示例处理器现在要么已经过时,要么在不久的将来有可能过时。第一版包含了一个完整的章节,展示了示例缓存设计,现在的设计者可以比当时更简洁地实现这些设计所需的芯片更少。在第一版的引言中,我说这一章"将比其他章节更早过时"(我比预期的更正确)。此外,这些示例中的几乎没有一种处理器还在外部使用缓存芯片,因为它们的速度更快的后继型号现在更适用于高性能系统。不过,我选择保留最旧的一个缓存示例,在第一章中进行演示,以说明真实的缓存设计的门数并不高,而且不必回避缓存设计。
我还添加了一个新的第五章,展示了其他人是如何应对遇到的某些难题的。看到其他人是如何绕过特别棘手的问题的,甚至可能是你自己会遇到的问题,是很有趣的。这些解决方案没有一个过于复杂,但它们显示了极大的创造力。
我已经在词汇表和文本中添加了一些词语;然而,这个领域似乎越来越向着统一,可能是由于现在缓存设计师之间的交流更多了。随着领域变得更加广泛,公司之间的交流变得更加频繁和开放,公司特定的行话被更标准化的行话所取代,人们发现自己需要发明一个新的行话之前,会先了解别人的行话。
最后,与我用来创建第一版的无缓存系统相反,第二版正在一台Windows 95机器上制作,该机器采用133 MHz Pentium处理器,有8KB的一级缓存和256KB的二级缓存。令人惊讶的是,我用来创建修订文本和图形的软件的新版本将速度减慢到了用户界面在新旧机器之间没有太大差异的程度。
INTRODUCTION TO THE FIRST EDITION
据说迪斯雷利曾观察到:"谎言有三种:谎言、该死的谎言和统计数据!"阅读本文的读者将有幸亲身了解最后一种类别,因为缓存设计是一门不精确的艺术,它基于不完全测量的统计前提,在某些软件性能基准中表现良好,但在其他情况下则失败。在整个文本中,我们将试图重新强调缓存性能与代码结构的依赖关系。
本书面向从事系统设计工程师,而非大学生。每章末尾没有问题。本书的目的是供自学和作为参考资料使用。如果你想学习特定方面的主题,可以在目录或词汇表中查找讨论该主题的页码。本书更像是一个实用的指南,而不是学术著作,因为它更多地采用定性而非定量的方法,这种方法可以通过缓存性能高度依赖正在设计的特定系统中使用的代码和硬件来证明。我通常会假设读者面临的挑战是设计一个缓存以优化使用现有CPU的系统性能,可能还有现有的总线和现有的代码。如果软件、总线和尤其是CPU能够被设计成与缓存和存储子系统的性能最佳匹配,那么可以使用几种独特的节省费用和时间的方法,但大多数系统并不适用于这个类别。
本书采用了一种直观而非分析的方法,因为除了本文之外,还有很多论文和论文支持几乎任何缓存架构的论点。这里的风格是非正式的、技术详细的,希望是可读的!我最不想让你们做的事情就是在词典中查找某个词汇。在这本书中,你不会找到"基础设施"或"范式"这样的词,章节名称也不以"On"开头(即"论基于计算得出的物质界面等高线波前边界效应")。
其实,缓存并不难理解。曾经在学校里被要求应用史密斯图表的工程师知道设计可以多么艰深。 (从我上大学的那些日子开始,我就对凡是姓史密斯的都心生怀疑。)另一方面,缓存设计非常直接,无论用多少行话来掩盖它。
基本原则是尽可能地作弊。我曾经教过一些把缓存当作一种需要学习的新语言的课程。背后的理论是,缓存设计师倾向于使用他们领域独特的术语。为了与设计组之外的任何人讨论你的设计理念,你需要学习这种行话。本书的术语表中列出了与缓存设计相关的250多个专用行话。术语表与首次定义该单词的章节和部分进行交叉引用。我费尽心思确保我所知道的每个行话都得到了清楚的讨论。阅读完本书后,你应该能够清楚地理解"全关联写透主逻辑缓存"和"直接映射扇区监听复制式二级缓存,带有分配写入、并发线路回写和通过包含来进行主要失效"之间的区别。
由于缓存语言的独特性和技术的新颖性,一个严重的劣势就是术语处于不断变化的状态,你经常会看到在文本中使用的词汇被完全不同地使用,或者更糟的是,用不同的词来描述相同的功能。一些设计社区(例如IBM和DEC)往往使用不同的流行词汇,与大多数其他缓存设计师不同。作者曾经认真考虑将这本书命名为《流行词汇:缓存设计书》。缓存不是唯一发生这种情况的领域。作者的一些软件倾向的朋友曾经愉快地称自己为"黑客",把系统破坏者称为"海盗",直到媒体混淆了事实并广泛地将"黑客"一词与臭名昭著的软件犯罪行为联系起来。英语是动态的,处于技术前沿的人注定要遭受后果。
本书仅涉及CPU缓存,而不涉及磁盘缓存。两种设备的操作原理类似,但是磁盘缓存通常是使用软件控制在动态RAM(DRAM)中实现的,而CPU缓存则以如此高的速度运行,必须使用硬件控制,并且缓存本身必须在静态RAM(SRAM)中实现。支持CPU缓存的统计数据已经得到了充分的发展,并与代码编写方式有关。磁盘缓存的统计数据完全不同,取决于特定软件在系统上运行时所引起的操作系统调用,并且不能直接从本文中介绍的任何内容中推导出来。
同样,本文中介绍的大多数映射算法也被用于内存映射单元(MMU),谢天谢地,它们被纳入了大多数现代CPU中。如果没有它们,大部分系统设计人员将被迫以两种不同的方式面对相同的问题。一些读者可能会在读懂本文后再次查看其系统中MMU的规格。
//CPU缓存和磁盘缓存的区别:
/*
CPU缓存和磁盘缓存是两种不同的缓存类型,它们的作用和机制也有所不同。
CPU缓存是指位于CPU内部的高速缓存,用于存储处理器频繁使用的数据和指令。CPU缓存可以大幅提高计算机处理速度,因为它比主存储器更快且响应更快。CPU缓存通常分为三级,其中一级缓存最快,容量最小,而三级缓存相对较慢,但容量更大。CPU缓存的容量较小,通常只有几百KB到数MB之间。
磁盘缓存则是指位于硬盘驱动器内部的高速缓存,用于存储读写硬盘的数据。磁盘缓存可以很好地平衡磁盘读写性能与容量之间的关系。当CPU需要从硬盘读取或写入数据时,数据将被缓存在磁盘缓存中,以便下一次读取时可以更快地访问数据。磁盘缓存通常由硬件控制器或操作系统管理,并且其容量通常比CPU缓存大得多,可以达到几GB甚至更多。
总之,CPU缓存和磁盘缓存都是用于提高计算机性能的缓存技术,但它们的位置、作用和容量都不同。CPU缓存是位于CPU内部的高速缓存,用于存储处理器频繁使用的数据和指令,容量较小;而磁盘缓存是位于硬盘驱动器内部的高速缓存,用于存储读写硬盘的数据,容量较大。
*/
NOTE: SOME ASSEMBLY REQUIRED
缓存设计师需要了解系统中正在执行的代码,这对于良好的缓存设计至关重要。本书将使用代码来说明支持缓存操作的基本现象。这并不意味着使用的算法很深奥,但我假设读者不会被文本中使用的非常简单的汇编代码示例所困扰。在设计过程中,鼓励设计者与其硬件设计团队的其他成员以及依赖于缓存设计性能的软件设计师进行讨论。这些软件设计师还可以尝试编写会导致缓存性能下降的代码,这不仅是为了友好竞争的精神,还可以熟悉应避免使用的代码结构,以最大化系统性能。
同样,我假设读者对处理器接口、系统总线(至少了解一个CPU, 多个直接内存访问[DMA]设备和通用背板)有很高的了解。如果这是一个问题,可以从处理器制造商、系统制造商以及教育和技术出版商那里找到大量的出版物,其中大部分都能很好地解决这个问题。
这本书分为五个章节。第一章是对缓存理论的介绍,接下来第二章概述了缓存架构。第三章回顾了RISC CPU的缓存需求,而第四章则着重讨论了一致性问题,并展示了商业可用机器中使用的技巧。作者花了很多心思来简洁地定义了当今与缓存相关的所有术语。
书的最后一章描述了一些现实生活中的离散缓存设计示例,这些设计支持商业可用微处理器。这些设计提供了非常详细的信息,以便进行仔细研究,设计师们可以借鉴这些示例来设计自己的缓存系统。
根据康涅狄格州纽黑文的专利数据库公司MicroPatent的数据,在1991年9月至1992年9月的一年间,仅在美国就授予了96项涵盖缓存设计的专利。显然,缓存设计是一门快速发展的技术,尽管我已经尽力跟上最新动态,但这本书无法跟上所有新的进展。请忽略任何遗漏之处。我希望有机会在未来的版本中加以补充。如果有任何批评或意见,欢迎通过出版社与我联系,以帮助改进或澄清未来版本的内容。
关于本书遵循的惯例,有几点需要说明。首次出现的流行词以粗体显示,并在词汇表中引用该词的唯一用法。我尽力确保任何单词的首次出现都附有其定义。数字有三种格式:十进制、十六进制和二进制。十进制是默认格式,任何具有超过三位精度的十进制数使用美国习惯,用逗号分隔(例如1,000)。十六进制数用于地址,并且以每四位数字之间有空格的方式分组(例如9ABC DEFO)。在极少数情况下使用二进制表示法时,它的格式将与十六进制数字相同,并且文本将解释正在使用二进制表示法。为了清晰起见,不会使用其他表示法(例如八进制)。缩写形式如8K和1 Meg代表常见的用法,即8K = 2的13次方,1 Meg = 2的20次方。
最后值得一提的是讽刺之处:本书的所有文本和图形均由作者在Macintosh Plus和Macintosh SE计算机上创建,这两者都没有缓存内存,也无法充分利用缓存,因为它们基于16 MHz的68000微处理器。是的,有些操作,特别是创建一些更复杂的图形和重新排版文本,如果使用更好、更快的CPU和缓存的机器,速度会快得多,也不会那么令人沮丧。
Chapter1 WHAT IS A CACHE MEMORY?
1.1 CPU SPEED VS. SYSTEM SPEED
问题很简单。设计师们不断努力以最具成本效益的方式发挥他们设计的最大潜力。当某个特定CPU的更快版本面世时,设计师通常会尝试通过简单地增加CPU时钟频率来提高现有设计的吞吐量。
在某个点之后,系统的主存储器(有时称为后备存储器)的速度成为系统吞吐量的限制因素。这在图1.1中有所体现。X轴表示CPU时钟频率,Y轴表示系统的总吞吐量。对于这个例子,我们假设系统是计算受限的;也就是说,系统的性能受到CPU性能的限制,而不是输入/输出(I/O)设备(例如磁盘驱动器)的速度。在计算受限的系统中,CPU缓存非常重要,因为它们仅用于提高与CPU相关操作的效率。

我们还假设,从CPU的角度来看,系统主存储器的性能在CPU时钟频率范围内是相同的。换句话说,如果主存储器的平均访问时间为60ns,这个访问时间将不会因CPU速度而改变。(即使在本文写作时,60ns这个数字也不算是特别快的速度,更何况几年后。作者要求读者在阅读文本时插入任何适当的数字,并预计问题将保持不变。)这个常数主存储器访问时间假设的价值将在本章1.2节中进行辩护。
在图表的早期部分,吞吐量与CPU时钟速度成比例地线性增加,直到标记为A的峰值。这很直观。突然间,它降到了一个更低的水平。为什么会这样?在时钟频率高达约20 MHz之前,处理器可以自由地尽可能快地运行,而没有来自60ns主存储器的等待周期。一旦处理器需要小于60ns的访问时间,必须插入等待周期来说明差异。单个等待周期足以满足,直到时钟频率需要小于60ns的访问时间,即使只有一个等待周期也是如此,这被标记为点B。再次,系统的吞吐量突然下降,就像添加第一个等待周期时一样。正如图表所示,增加时钟频率仅仅将系统的吞吐量通过Figure 1.1中的锯齿状图案表示的狭窄波段,而没有任何提高性能的希望,这个波段的上限由内存访问时间设定。更糟糕的是,锯齿状图的峰值和低谷趋势随着系统时钟频率的增加而减小,这是我不确定是否完全可以解释的现象。
当然,通过使用快速主存储器可以解决这个问题。仅剩的问题是,要支持最快的操作,必须使用什么速度的主存储器?
1.2 THE COST OF FAST MAIN MEMORY
快速的主存储器价格昂贵。一般规律是,一个快速存储芯片与下一个速度等级之间的价格差异可以高达50%或100%!所有这些只为了获得额外20%的速度!如果这个世界是一个公平的地方,在动态随机存取存储器(DRAM)上获得20%的速度增长应该支持CPU在速度上的20%增长。然而,实际发生的事情远非如此。大多数系统在CPU和DRAM之间插入一些缓冲区,另外在DRAM数据输出与CPU数据输入引脚之间插入另外一些缓冲区(Figure 1.2)。

//DMA Device和Main Memory不是一个东西?
/*
DMA设备(直接内存访问设备)和主存储器是不同的东西。
DMA设备是一种特殊的硬件设备,它可以绕过CPU,直接与主存储器(DRAM)进行数据传输。它可以读取或写入主存储器中的数据,而无需CPU的干预。常见的DMA设备包括网络适配器、硬盘控制器、图形卡等。DMA设备通常用于高速数据传输,以提高系统性能。
主存储器是计算机系统中用于存储程序和数据的物理存储器。它是CPU可以直接读取和写入的内存空间。主存储器存储着正在运行的程序、处理过程中的数据和其他临时存储的数据。
虽然DMA设备可以访问主存储器,但它们并不是同一个东西。DMA设备是用于数据传输的特殊硬件设备,而主存储器则是存储程序和数据的物理存储空间。
*/
//DMA device对应的DMA Controller是软件还是硬件?
/*
DMA(Direct Memory Access)是一种计算机系统中的技术,它允许数据不通过CPU直接在内存和外设之间传输。DMA设备是一个专用硬件,它负责管理和控制数据传输,但是,DMA设备本身并不足以完成数据传输,需要有一个DMA控制器协同工作来完成数据传输任务。
DMA控制器通常是一个独立的芯片或模块,它可以是硬件实现,也可以是软件实现。在硬件实现中,DMA控制器是一个物理设备,它与CPU和其他外设相连,可以通过寄存器或总线接口进行配置和控制。而在软件实现中,DMA控制器是由操作系统内核或相关驱动程序实现的,它通过编程方式控制DMA设备的行为,在内核层面管理DMA传输。
总的来说,DMA设备和DMA控制器都是硬件实现的,但是DMA控制器可以是硬件或者软件实现的。在硬件实现中,DMA控制器通常是一个单独的芯片或模块,而在软件实现中,则由操作系统内核或相关驱动程序负责实现。
*/
这些缓冲器不仅增加了CPU驱动系统中许多DRAM地址输入所需的小电流,还在另一个设备需要控制主存储器的那些时候,将CPU与内存隔离开来。这些设备的传输延迟是固定的,也就是说,这些设备的传输延迟并不像DRAM和静态RAM(以后我们将称之为SRAM)的存储时间那样随着半导体工艺的进步急剧下降,也不如微处理器的时钟速度增加得快。其他静态时间是写周期数据和地址设置和保持时间(相对于写脉冲边缘),这些时间通常对任何速度级别的DRAM或SRAM都是相同的。一个典型的SRAM可能有约5ns的写数据设置时间和1ns的保持时间。问题在于,尽管制造商试图在其更快速的零件规格中减小这些数字,但改进远不成比例地等于读取访问时间的任何改进。因此,在整个CPU时钟周期时间中被这些静态时间消耗的比例越来越大。在缩小的时钟周期内,如果去除了这些静态设置和保持时间,则为了满足CPU的需要,用于内存访问的时间必须比时钟周期缩短得多。

图1.3展示了一个无缓存处理器系统的时钟频率与内存子系统所需访问时间之间的关系,以支持该系统上的零等待读取周期。对于此示例,使用了图1.2所示的系统,基于386处理器,因为该处理器没有内部缓存,所有缓冲区提供5ns的传播延迟,并且系统中没有由引脚电容或PCB负载引起的其他延迟(这些将在第2章2.5节中详细介绍)。为了公平起见,我包含了386的两种工作模式,流水线和非流水线。对于任一情况,趋势渐近地接近1ns的访问时间,但流水线操作的计时在更高的时钟频率下达到零。
1.3 THE CONCEPT OF LOCALITY
我们都是非常务实的设计师。当被要求设计一个符合特定目标规格的系统时,我们试图以最低合理的成本来实现规格要求。前面两节中提出的论点似乎表明,设计一个能够利用当前可用最快CPU速度的系统将变得代价过高。
这就是统计学派上场的地方。在20世纪60年代,IBM的研究人员发现几乎所有代码都具有极高的重复性,这一事实可以用于提高计算机吞吐量的优势。如果任何重复性的内容都可以存储在小型、高速的存储器中,那么等待状态的影响只会限制在程序中较少重复的部分,而这些部分可以存储在更慢、更便宜的存储器中。
程序的哪个部分是重复的?这个问题通过局部性原理来回答,包括空间局部性和时间局部性。
1.3.1 Locality in Space
空间局部性或引用局部性是指大多数计算机代码在一个小区域内重复执行的现象。这个空间不一定在主存储器的单个地址范围内,而可以相当分散地分布。

为了说明,图1.4展示了一个汇编代码程序。这个程序并没有以任何严格的方式编写,但足以展示空间局部性的一般概念。该程序是所有计算机中都存在的代码类型的简化版本。首先,一个循环被用来遍历存储在内存中的一个99个字符的字符串。其次,一个子程序包含了一段可能对主程序的其他部分有用的代码。在进入和退出子程序时,栈被用来临时存储和检索调用程序的程序计数器(PC)。这段代码的功能是扫描一个字符串,找到大写字母"A"并将该字符串中所有出现的大写字母"A"替换为小写字母"z"。
第一个非常明显的事实是,代码被编写成在处理器通过循环时反复使用相同的一组指令,主要是子程序内部的指令。空间局部性原则指的是调用程序、子程序和堆栈空间都存在于从地址00001000(调用程序)、00011008(子程序)和F0000007开始的三个非常狭窄的区域内。
//一个程序的代码段对应的物理地址应该是连续的(so有自己的连续物理地址空间),申请的动态内存对应的物理地址范围是?局部变量对应的栈地址范围是?
1.3.2 Locality in Time
第二个局部性原则,时间局部性或时间引用,仅仅指的是这个例子中的指令按照接近顺序执行,而不是在时间上被分散。处理器更有可能需要访问它10个周期之前访问过的内存位置,而不是10,000个周期之前访问过的位置(在166 MHz Pentium上只有0.12毫秒)。这似乎非常明显,但如果没有空间和时间的局部性,缓存将无法工作。
结合空间和时间的局部性,可以简单地说明,在这个例子中,程序将在执行相同的几条指令99次后花费一段时间,然后转移到另一个代码片段,这个代码片段可能也会被重复。如果读者查看一个真正的程序,一个比这里使用的示例更长的程序,将会发现与这个示例大小相似的重复序列位于更大的重复循环内,而这些循环又位于更大的循环内,如此循环下去。
利用时间和空间的局部性是个好主意,可以确保程序的重复部分在使用时从非常快的内存执行,并在等待使用时驻留在较慢、成本较低的内存中。乍一看,最明显的实现方法是将内存空间分为快速和较慢的部分,并让操作系统根据需要将代码的部分从较慢的部分复制到较快的部分。这正是虚拟内存系统的工作原理,其中较慢的内存是磁盘等大容量媒体,而较快的部分是主内存或DRAM。另一种方法是除了完整的较慢主内存之外,还拥有一块小而快速的内存,并使用专门的硬件来确保主内存中当前有用的部分被复制到这块小而快速的内存中。这就是我们所说的缓存内存。
基于软件的技术在内存映射方面非常优秀、经济实惠,并且在内存映射方面运作良好,但它并不适用于缓存管理,原因有两点。首先,成本考虑通常会使缓存非常小,通过软件将内存空间映射进出缓存会消耗过多时间,从而抵消了缓存的速度增益。(通常需要一个至少与我们在图1.4中示例循环的大小相当的软件来将示例循环移入快速内存,而小而快速的内存则需要相对频繁地替换其内容。)其次,大部分缓存被设计用于加速已有的系统,在这些系统中,操作系统和其他软件无法重新配置以支持分割为快慢部分的内存。在这种系统中,软件必须对缓存的存在一无所知。所有现有程序在缓存系统上都必须以类似但更快的方式运行。实现这一目标的缓存称为软件透明缓存。
1.4 FOOLING THE CPU
因此,设计一个内存控制器,在硬件层面上在慢速和快速内存之间来回传输数据,让CPU始终认为正在访问相同的地址,只是有时候这个位置的访问速度比其他时候快。虽然以这种方式欺骗CPU听起来很棘手,但实际上并不是。一旦读者理解了基本概念,简单的缓存设计几乎变得微不足道。

我喜欢用一个同事曾经告诉我的类比来解释,就像是一个文员的桌子旁边放着大量的文件柜,可能是在一个非计算机化的信用调查机构(见图1.5)。文员的平凡工作是接听电话并根据来电者的查询从信用申请人的档案中阅读条目。文件按照申请人的电话号码进行存储(为了方便论述,我们采用美国风格的七位数电话号码)。在一个典型的工作日里,文员会接听50到100个电话,并会从桌子旁边站起来检索文件50到100次。然而,由于同一个信用申请人可能在同一天或两天内在五到十家银行申请贷款,其中几个电话的请求数据与前一个电话或几天前的请求数据相同。(敏锐的读者可能会注意到,这个类比使用了时间局部性,但忽略了空间局部性。)
现在假设文员非常聪明,他看到办公桌的膝洞里还有一个未使用的文件抽屉。为什么不在桌子的文件抽屉中保留那些当前需求较高的文件的副本呢?复印机就在文件柜旁边,所以复制一份回办公桌上没有问题。但谁知道哪些文件会被重复请求,哪些文件只会被请求一次呢?无法预测未来,所以文员会简单地将每个从文件柜中取出的文件都复制一份。
这开始非常容易。当需要某个文件时,文员将其副本放入办公桌的文件抽屉,并在挂在墙上的一张纸上记录这个事实。这张纸是办公桌文件抽屉的目录。接听电话时,文员首先查看目录,确定办公桌文件抽屉是否有所需文件的副本。如果有,文员不需要站起来,只需从抽屉中取出该文件即可节省时间和精力。目录还列出了在文件抽屉中可以找到某个特定信息的位置。在无法在文件抽屉中找到文件副本的情况下,文员将站起来,从文件柜中复制文件,并在目录上做出相应的记录。
一切都运作得很好,直到文件抽屉变满了。那时怎么办?文员非常实际,决定在每个目录条目旁边加上最后访问日期和时间的记录。当新文件放入办公桌文件抽屉时,具有最旧日期和时间的目录条目将被擦除,文件将被丢弃,并将新的文件和目录条目放在相同位置。(在另一个有趣的类比中,Intel曾将同样的问题比作去超市买东西但更喜欢将经常使用的物品存放在家里的冰箱里。)


这就是缓存内存设计的概念。庞大的文件柜银行代表系统的主内存,而文员则代表CPU。办公桌文件抽屉称为缓存数据内存或数据RAM,目录恰如其分地称为缓存目录。刚刚描述的确切实现很少使用,因为硬件要求比一些非常好的替代方案更昂贵。在确切的缓存实现中使用了几种技巧,本书的相当一部分内容将用于介绍这些技巧。
缓存内存有四个基本部分,如图1.6所示。 缓存数据内存是小型、快速的内存,用于存储指令和数据的副本,这些指令和数据从主内存访问时速度较慢。当讨论缓存的大小时,缓存大小等于仅缓存数据内存中字节的数量。目录不包括在此数字中。缓存目录是相应位置的缓存数据内存中存储的数据的主存储器地址列表(即,如果某个缓存数据位置中的数据来自主存储器地址0000 0000,则缓存目录将包含0000 0000的表示)。因此,每个缓存位置都存储数据,同时也存储地址,使得组合的目录和数据RAM表现得像单个非常宽的内存。
总线缓冲区具有非常重要的功能,尽管它们基本上与图1.2中显示的非缓存系统中使用的芯片相同。这些缓冲区现在被控制的方式是,如果缓存可以提供主存储器位置的副本(这称为缓存命中),则不允许主存储器将其数据放入CPU的数据引脚上。在许多缓存设计中,除非缓存已指示其不包含CPU请求的数据的副本(缓存未命中或有时为故障),否则永远不会发送地址到主存储器。因此,在所有缓存命中周期中,CPU与缓存通信,在所有缓存未命中中,CPU与主存储器通信。 CPU、缓存数据RAM和系统总线缓冲区之间的数据总线有时被称为缓存的数据路径。
整个系统的关键是缓存的控制逻辑,也称为缓存控制器或缓存管理逻辑。这个逻辑实现了将数据从缓存数据内存和缓存目录中移入和移出的算法。这是缓存设计的关键所在,缓存逻辑体现了一些备受争议的决策,被称为缓存策略。一旦决定并实施了某种策略,它就成为了缓存的政策。在第二章中,我们将详细讨论这些策略。控制逻辑还确定何时打开和关闭总线缓冲区,并确定何时从缓存数据RAM读取和写入。在一些系统中,系统总线接口足够复杂,以至于缓存控制器的系统总线端被细分为其自身的一部分,这时它可能被称为存储控制单元(SeE)。
缓存大小和策略都会影响缓存的命中率(CPU周期中缓存命中的百分比)。相反,未命中率是CPU周期的剩余百分比。基于每个总线事务的平均等待周期数(像是访问DDR的cpu cycle),可以计算缓存系统的吞吐量。该数字等于未命中率乘以系统总线上的平均等待周期数(再加上命中率乘以缓存延迟时间,如果缓存需要使用等待周期)。
举个例子,假设所有的系统总线访问都需要三个等待周期,而缓存在零等待周期内响应。对于一个命中率为90%的缓存内存(很容易实现),缓存系统的平均等待周期数将会是 10% * 3 = 0.3 个等待周期。在这种计算中,小数位的等待周期是完全有效的,因为这是等待周期和零等待周期之间的平均值。同样的系统如果使用更小的缓存,可能只能达到80%的典型命中率,那么平均等待周期数将是较大缓存的两倍,即0.6个等待周期。如果使用速度更慢的DRAM,需要更多的等待周期才能完成总线周期,那么吞吐量的提升会更加显著。
另一种有趣的思考方式是将问题扩展到所有系统时钟周期的消耗。假设我们有一个处理器,其平均零等待指令执行时间为两个周期,而缓存未命中需要三个等待周期,总共需要 3 + 2 = 5 个周期。在一个命中率为80%的缓存中,系统中的十条指令将消耗 10 * 0.8 * 2 + 10 * (1 - 0.8) * (2 + 3) = 26 个周期,其中只有16个周期能够从缓存中执行。换句话说,CPU将尝试处理20%的指令却花费了将近40%的时间。这也意味着总线将在40%的时间内被占用,对此我们将在第1.8节进行探讨。
缓存的工作过程遵循一定的模式。在上电时,缓存包含随机数据,并不允许对CPU的请求做出响应。当处理器从主存储器中读取数据时,缓存数据RAM会被命令复制该位置的内容,而相应的缓存目录位置被告知复制CPU请求的地址。这个过程对任何后续周期也将发生,直到遇到一个循环为止。一旦处理器到达循环的末尾,它将再次输出第一个位置的地址,这很可能仍然存在于缓存数据内存中。然而,这一次,该位置的数据将从缓存中提供给处理器,而不是从主存储器中提供,与循环中的其他指令一样。这显然会有多大的帮助。在我们的示例程序中,循环中的指令只有在第一轮中以主存储器速度运行,然后接下来的98次循环中将更快地从缓存中执行。
这涵盖了缓存读取周期。在写入周期中发生的情况取决于所选择的缓存策略,并将在第二章中进行深入探讨。从CPU的角度来看,数据始终来自主存储器,尽管有时候它会比其他时间到达得更快。
1.5 CACHE DATA AND CACHE-TAG MEMORIES
现在我们知道了如何简单地将CPU的主存储器请求重新定向到缓存来满足,但是缓存目录和缓存数据内存的设计还没有解释。对于初学者来说,这可能会很吓人。目录体系结构的选择对缓存数据内存的设计有很大的影响,因此首先将对目录进行研究。
某些大学级课程解释了缓存目录由内容可寻址存储器(Content Addressable Memory,CAM)组成。这是一种目录类型,与我们在信用局中的职员的例子非常相符。CAM是一种反向存储器,当数据被提供给特定输入时,它输出一个地址。该地址显示在CAM内找到匹配条目的位置。所有CAM位置同时进行匹配数据的检查,如果找到匹配项,其地址将被放置在地址输出引脚上。当需要将32位处理器地址转换为较小的缓存地址时,这特别有用。32位处理器地址被呈现到CAM的数据输入引脚,并且一个较短的地址从地址输出引脚中弹出。事实上,几乎从不使用CAM来实现缓存设计,但它们便于解释缓存目录,因此我们将首先研究CAM,然后再看看替代方案。

从中有一个重要的问题是"什么是CAM?"CAM由一系列地址寄存器组成,每个寄存器都包含一个比较器,用于将寄存器中包含的地址与当前在比较地址总线上的地址进行比较(见图1.7)。这通过为每个地址寄存器附加一个单独的比较器来实现。
尽管听起来需要大量硬件,但实际上可以使用专门的七晶体管静态RAM单元在硅中相对容易地制造,其复杂度不到工业标准四晶体管单元的两倍。比较器输出被简单的优先编码器编码为匹配条目地址的二进制表示。在写入周期期间,CAM会同时接收地址和数据,并生成写脉冲。然而,在读取周期期间,数据输入到CAM中,并输出地址。输出地址是包含匹配数据的CAM位置的地址。如果没有找到匹配数据,则CAM输出无匹配信号。
读者可能会问"为什么CAM并不普遍可得?"简单的原因是,比CAM方法表现几乎相同的更简单方案可以使用标准的静态RAM构建,并且可以使用更便宜的缓存数据RAM。这些替代方法将在此简要讨论,并在第2章中深入探讨。缓存设计师不愿意为CAM付出与CAM吸引半导体制造商兴趣所需的成本一样多。作者只知道市场上有两种CAM可用。第一种是一种成熟产品,一种ECL 4 x 4位设备,本来将被设计用于大型机缓存,但后来速度更快(70ns)的静态RAM可用。另一种是专门为局域网地址过滤器设计的设备,在相对较长的时间内(即10ns)需要输入48位比较地址的三个复用块。

回到例子中:假设系统刚刚初始化,缓存位置中没有包含主存位置的有效副本。在缓存中应该放置处理器访问的第一个位置?有些处理器会在复位后从第一个主存地址或地址零开始寻找其第一条指令。其他处理器从内存空间的顶部(FFFF FFFF)开始。还有一些处理器在这两个地址中寻找一个向量,然后跳转到该向量指向的地址。
如果缓存目录是一个CAM,那么任何主存地址都可以映射到任何目录位置。这种方法称为全关联缓存,这个术语简单地意味着任何主存地址都可以在缓存内的任何位置复制。用于确定哪个缓存位置应该用于存储主存地址副本的方法称为映射或散列(我们将在本书中研究一些散列算法)。每当要将某物放入缓存时,将不得已(希望未使用)的目录位置分配给它。缓存控制器通过将表示要用于存储输入数据的目录位置的地址分配给CAM和缓存数据内存来执行此操作,在访问主内存时进行。同时,将从主内存访问的地址(完全独立于刚提到的目录地址)中包含的数据写入目录,同时将主内存数据馈送到CPU并写入缓存数据RAM(见图1.8)。稍后,当处理器的地址再次与CAM中存储的地址匹配时,CAM输出包含匹配数据的缓存数据RAM位置的地址。该地址被馈送至缓存数据RAM,其由标准静态RAM组成,该地址中的数据可供CPU使用。
由于大型CAM的缺乏,人们设计了几种巧妙的CAM方法替代方案,这些方案也往往比CAM方法要简单得多。回到档案员的比喻上来,我们可以设计一个组织系统,利用抽屉的尺寸与所有文件柜中的抽屉尺寸相同的事实。使用的电话号码都是七位数字,并遵循格式867-5309。此外,假设文件柜是按前缀(电话号码的前三个数字)排列的,每个抽屉可以携带该前缀范围内所有10,000个号码或信用申请的文件(从 -0000 到 -9999)。因此,第一抽屉将具有000-0000至000-9999等数字。文员还可以将桌子抽屉分成10,000个插槽,以便每个插槽只允许包含与插槽号匹配的后缀(最后四位数字)的文件,换句话说,号码867-5309只能放在桌子抽屉插槽5309中。目录仍然是墙上的纸,其中包含10,000个条目,描述存储在每个桌子抽屉插槽中的数据的电话号码;但是,每个条目都位于与电话号码的后缀匹配的位置,因此只需要将前缀写入目录中(因此对于号码867-5309,将前缀867写入第5309行)。此外,文员无需再选择放置新缓存条目的位置,因此目录条目不再需要包含其上次访问的时间。这是今天缓存设计中最流行的散列算法,称为集合关联缓存(组相联缓存),因为为了清晰起见,电话号码的最后四位数字已被命名为缓存的集合地址,在任何集合内部,条目都是关联的(可以有任何前缀)。
通过这种新方法,文员可以在查阅目录的同时开始查看桌子抽屉中的文件。不再需要交叉参考目录以确定哪个桌子抽屉位置包含该文件。如果正确的集合地址(5309)上的前缀(867)与来电者请求的号码匹配,那么文员几乎可以立即从桌子抽屉插槽5309中取出文件。在集合关联设计中,处理器输出的地址被分割成前缀和后缀的等效部分,这个位置是由缓存的大小和架构确定的。低地址位,即与示例中电话号码的最后四位数字对应的位,被称为集合位,因为它们包含集合地址。有些人将其称为索引位,但这个术语与虚拟内存转换中使用的术语相冲突,为了清晰起见,在本文中将不使用这个术语。剩下的(高位)与所选集合的目录条目进行比较,被称为标记位。
你可能还记得我在最初描述文员类比时提到过的一句话,即这个类比没有考虑到局部性。这很容易理解,因为文员随时可能接到任何文件的电话,代表任何电话号码,以任何顺序。计算机代码从不以这种随机的方式运行,而往往充满了循环和重复,并且只要程序计数器不在循环或子程序调用上跳来跳去,代码就会按顺序执行。集合关联方法利用了这种空间局部性,将顺序指令放置在不完全随机的缓存位置上,而是在连续的位置上。正如刚刚演示的那样,集合关联缓存不允许将主存地址映射到任何缓存地址,而是限制缓存,使得缓存位置的低地址位必须与匹配的主存地址的低地址位相匹配。由于处理器将遍历一小组连续地址,这看起来应该没有问题,而且集合关联缓存应该与更复杂的全关联设计一样高效。但在第1.6节中我们会看到情况并非如此。

前面的段落提到,完全关联缓存(具有CAM)对比集合关联缓存需要使用更昂贵的缓存数据RAM(因为性能原因,需要选择更快的RAM)。为什么呢?在完全关联缓存中,所有地址位在输入缓存数据RAM之前都要通过目录进行过滤。这意味着在缓存CPU的最快周期------缓存读命中时,处理器地址必须首先经过目录CAM并转换为缓存地址,然后该地址必须通过缓存数据RAM才能将读取数据提供给CPU(参见图1.9a)。这意味着处理器输出地址与数据从缓存返回之间的延迟至少等于CAM和RAM的传播延迟之和。因此,如果需要在处理器内返回数据,比如说30纳秒以内,那么这30纳秒必须被分配给CAM和RAM。现在的150 MHz微处理器要求这个过程在不到20纳秒内完成。拥有8纳秒的CAM(祝你好运!)和8纳秒的缓存数据RAM的系统将难以在这个关键路径上使用任何逻辑。
关联集高速缓存将CAM(内容地址存储器)和RAM(随机存取存储器)的延迟减少为单个RAM访问。在1.9b图中,CPU地址直接连接到缓存目录和缓存数据RAM。为了简化设计,所示的设计使用与缓存数据RAM相同深度的标准静态RAM作为缓存目录。(关于此的其他选择将在第2章中详细解释。)假设缓存深度为8,128(8K),需要13位地址才能完全访问。然后,较低的13位CPU地址同时路由到缓存目录和缓存数据RAM。当缓存数据RAM找到与匹配的较低地址位(但不确定哪些上位地址位)存储在主存储器位置上的数据副本时,目录则查看存储在具有相同较低地址位的位置上的副本的上位地址位。这很令人困惑,但基于桌子类比,请记住电话号码867-5309的最后四位是5309,因此在文件抽屉中访问槽5309。然后,目录必须告诉我们是否存在存储的前缀(867)与正在访问的其他前缀号码(984-5309)是否匹配。为了解决延迟问题,要注意在处理器周期内同时检查缓存目录和缓存数据RAM,缓存目录的输出只是用来告诉CPU是否以全速(命中)继续运行,还是等待主存访问来获取CPU请求但不在缓存中的数据。
//上面讲的很清晰!

关联集高速缓存中的目录比全相联高速缓存中的目录简单得多。全相联高速缓存的目录不仅需要跟踪是否存在一个有效条目,该条目包含在缓存中的主存储器位置(这个事实尚未描述,但将在第2章中描述),还需要跟踪该条目在缓存数据RAM中的位置,而关联集目录只需要查看具有与CPU输出地址相同集位的缓存条目是否来自具有相同标记位的位置,并且该位置是否有效。这是一个简单的比较。位置5309上的标记是否与电话号码867-5309的867匹配,并且位置是否有效?显然,可以使用异或门输入到与门进行标记比较,这是一个简单地址比较器芯片的功能(图1.10)。有效性同样简单,也将在第2章中详细描述。在处理器的读取周期中,关联集高速缓存假设(除非另有说明)缓存中的数据就是处理器正在请求的数据。缓存数据RAM启动读取周期时,将其数据输出打开,并且其地址引脚直接与处理器的较低有效地址输出引脚连接(参见图1.9b)。同时,缓存标记RAM正在查找地址的更高位,以查看缓存条目是否来自正确的地址。如果所请求的地址与主存储器数据的缓存副本的地址匹配,则缓存控制器向处理器发出准备信号以接收来自缓存数据RAM的数据,并允许继续进行。如果缓存中具有与某一特定集地址相同的来自主存不同部分的数据(即,标记位不匹配),则缓存控制器通过不断言准备输入来声明发生了缓存未命中,然后将主存储器数据读入缓存,允许CPU继续进行,同时主存储器数据和地址被写入CPU。关联集高速缓存中的缓存数据RAM只需具有略小于处理器最小地址到数据周期的访问时间。缓存标记RAM的访问时间也会稍慢一些,减去比较器和下游逻辑所需的延迟。较慢的静态RAM通常比较快的静态RAM便宜得多,因此设计者可以通过采用关联集设计节省很多费用。一些静态RAM制造商提供的产品中包含在标准SRAM内部的比较器。这样做在速度、组件数和下游逻辑复杂性方面具有某种优势。无论目录是由一个简单的内部包含比较器的RAM还是具有两者的芯片制成,关联集高速缓存中的目录通常称为缓存标记存储器,因为它是存储地址标记的位置。还有其他几个术语用于描述缓存标记RAM,特别是标记RAM、缓存地址比较器、地址比较器、缓存比较器或简称比较器。
1.6 THRASHING: GETTING THE MOST OUT OF THE CACHE
//thrashing:抢占
在前面的部分中讨论的整个论点忽略了一种称为"thrashing"的现象。当缓存 thrashes 时,一个常用位置被另一个常用位置所替代。每当 CPU 在缓存中找不到所需内容时,它必须以主存储器较慢的速度执行主存储器访问操作。这使得这些访问至少与没有缓存时一样慢。
假设处理器必须按照紧密的序列检查两个地址,这些地址包含相同的集合位。这是如何发生的?有几种方式。首先,大多数代码都由调用例程和子例程构建。子例程中的某些代码可能具有与调用例程中的代码相同的集合位,因为调用例程可能与子例程无关。对于集合位较少的小缓存来说,这尤其正确,这使得冲突的可能性更高。接下来,考虑调用子例程时会发生什么。程序计数器和可能还有很多其他寄存器被推入堆栈。堆栈在哪里?堆栈的集合位与调用例程和子例程中的代码的集合位匹配的可能性有多大?集合位可能会混淆的另一个地方是当程序使用指针时,比如移动值块或检查文本字符串。最后,考虑当中断被服务时会发生什么。中断服务例程(或由该服务例程调用的某个子例程)是否具有与调用例程地址的集合位匹配的集合位?这些任何一种情况都可能导致缓存 thrash。



//汇编指令JSR什么意思
//使用 JSR 指令时,程序会将当前的执行地址保存在栈中,并跳转到指定的子程序开始执行。子程序完成后,通过返回指令(通常是 RTS 或 RET)返回到原调用位置继续执行。
例如,让我们重新审视图 1.4 中所示的代码片段。三个地址具有 008 十六进制的最后三位数字:调用例程中循环的开头,堆栈指针和子例程的开头。如果我们的缓存使用这三个最后三位数字作为集合地址,或者使用这三个数字的子集,则缓存将 thrash。下一个段落和图 1.11 将详细说明这一点。在这种情况下,堆栈指针是一种"陷阱",因为它会导致 CPU 覆盖缓存中的其他项。我之所以指出这一点,是因为很多设计师倾向于忘记推送和弹出项目的指令会执行读取和写入操作,您必须考虑才能记住这些操作。对于缓存设计师来说,总是知道堆栈指针的位置是一种豪迈的表现,尤其是在多任务操作系统下。
首先,缓存加载地址0000 1000,即"加载堆栈指针"的指令。它被放置在缓存位置000中,并在标记位置000处写入标记地址0000 1。所有这些都发生在主存储器周期时间内,因此缓存不会加速这个特定的周期。类似地,接下来的两个指令也以主存储器访问时间加载到缓存地址004和008中,并且这两个新缓存副本的标记也被填充为在标记地址004和008处的0000 1。位于地址0000 1008的指令是一个"跳转到子程序"(JSR),因此程序计数器被推入堆栈位置F000 0008,并且在这个例子中的缓存设计中,将立即覆盖存储在内存地址0000 1008(JSR指令)的指令,再次以主存储器访问的较慢速度进行。进入子例程后,从地址0001 1008获取子例程的第一条指令,再次以主存储器访问时间获取,并立即替换存储在缓存位置008处的程序计数器的新缓存副本。当遇到返回指令时,推入的程序计数器不是从缓存中恢复,而是从主内存恢复,因为它被子例程的第一条指令从缓存中弹出。对于此堆栈地址,没有实现缓存速度优势。最后,重新进入调用循环,并再次从主存储器获取地址0000 1008处的JSR指令,而不是从缓存中获取,再次以较慢的主存储器周期时间进行。这三个项目在缓存中不断替换彼此的整个过程重复了99次。显然,与008匹配的较低地址位的位置根本没有从缓存的使用中受益!(其他地址(除了00C)缓存收益了)
这个例子的主要观点是,小型缓存中频繁发生的抖动,在更大的缓存中逐渐不再重要,因为位设置数量增加,导致地址之间的位设置混淆减少。你可能想考虑两个极端的论证:在只有一个位设置(地址的最低有效位)的系统中,抖动有多大可能性?在具有与 CPU 地址输出数量相同的位设置(缓存大小与最大可用主存储器一样大)的系统中,抖动可能性有多大?第二个观点是,抖动是集合关联缓存的现象,其中的缓存行根据其位设置而不是基于某种更合理的算法进行覆盖写入。全关联缓存将会像处理没有任何地址位匹配的事务一样轻松处理刚才演示的问题。在第2章中,我们将探讨介于集合关联和全关联模型之间的缓存体系结构。与抖动相关的另一个术语是必然失效。无论缓存设计有多好,都会发生一些缓存失效,简单地因为缓存被要求提供此前没有包含的数据。由于无法避免这些失效,它们被称为必然失效。抖动从不导致必然失效。然而,和本书中的大多数术语一样,还有一些替代术语可以用来描述必然失效,例如稳定失效和非冲突失效。
1.7 CACHES AND THE MEMORY HIERARCHY
内存层次结构或存储层次结构指的是内存的价格、大小和性能在不同阶段的变化。计算机中最昂贵的内存是处理器内部的寄存器。这些设备被设计成执行非常快速,并且它们的结构允许在一个寄存器被写入CPU的同时,CPU同时读取一个或多个不同的寄存器。该结构每个位需要消耗多个晶体管,并且成本和速度与每个位所需的晶体管数量有关。寄存器的访问是通过片上总线进行的,因此寄存器数据的访问时间不受离开芯片的信号要求的限制。大多数处理器仅具有少量寄存器,或者只有几十个寄存器,这些寄存器的宽度等于芯片内的数据路径。寄存器的访问时间通常低于2纳秒。
在大多数非缓存系统中,下一级内存是主存储器。主存储器通常由动态随机存取存储器(DRAM)组成,其访问时间是寄存器访问时间的几倍。动态随机存取存储器使用每个位一个晶体管的方式制造,支持电路需要额外的少量晶体管。DRAM的结构使其成为最便宜的半导体存储器形式。在当代基于微处理器的系统中,典型的DRAM主存储器容量可能在16到512兆字节之间。目前,主存访问时间大约为70纳秒。
下面的两个级别由旋转存储介质组成。磁盘通常用于存储无法保存在主存储器中的数据。如今的硬磁盘访问时间在几十毫秒左右,容量从几百兆字节到数千兆字节不等。硬盘通常与可拆卸介质备份,这些介质通常是磁带或软盘。这两种介质都比硬盘便宜得多,但访问时间需要几秒钟。由于这个级别是可拆卸的,所以其大小只受用户保留磁带或软盘的意愿限制。
缓存内存位于上述两个级别之间。现今的一些微处理器包含一个内部缓存,由标准的静态RAM位构成。与寄存器不同,这些位置不能同时写入和读取,因此在芯片上的缓存中进行某些操作时,会更慢一些。一个寄存器位可能需要十个或更多的晶体管来实现,而用于内部缓存的SRAM单元可以包含每位4到6个晶体管。由于微处理器使用相同的芯片制造,芯片上的缓存大小受到限制,主要是为了避免处理器的成本飙升。芯片成本与芯片大小并不成比例,而是以二次以上的速率增加。芯片上缓存共享不需要离片访问的优点,因此它们非常快速。现今的微处理器通常包含8到64K字节的缓存。
在具有芯片缓存和没有芯片缓存的系统中都使用外部缓存。连接到未缓存的处理器的外部缓存用于加速主存储器访问,如上所述。这样的缓存通常从256K字节到16M字节不等,使用经济、由每位4个晶体管构成的SRAM芯片制造的四志存器单元内存单元。缓存的访问时间大约为3到15纳秒。如果处理器具有芯片缓存,则通常会使用与芯片缓存架构相差很大的体系结构来实现任何外部缓存,这是为了降低成本,并弥补芯片缓存的一些不足之处。这些设计问题将在第2章中进行探讨。

表1.1以美元规模和带宽显示了一个高性能缓存系统的典型存储层次结构,其处理器使用芯片上的缓存。
1.8 REDUCING BUS TRAFFIC
在本章中尚未提到的添加缓存到系统中的一个原因是:多处理器可以利用缓存内存来提高其主存储器的有效总线带宽。总线带宽,也称为总线传输或简称流量,是以每秒字节数衡量的在总线上移动数据的最大速度。例如,一个字宽为16位(2字节)的总线,可以每秒传输高达800万个字,其带宽为2 x 8 = 16兆字节/秒。由处理器使用的总线带宽百分比被称为利用率。(类似于主存储器带宽,缓存到CPU接口有自己的带宽,称为缓存带宽。如果缓存带宽低于CPU所需的带宽,CPU必须通过降低时钟频率或添加等待状态来减慢速度。)

我们将很快看到,使用缓存内存来减少处理器对主存储器带宽的需求是一个非常重要的好处,并且随着多处理器体系结构作为显著提高系统性能的手段日益被广泛接受,这个好处将变得更加重要。如果多个处理器的利用率接近100%,它们可能会遇到麻烦,并在达到100%限制后性能不如较少处理器的系统。
存在两种多处理器系统,如图1.12a和b所示。顶部示意图显示了称为松散耦合多处理器系统或分布式存储多处理器(DMM)的系统。松散耦合系统实际上是两个或多个不同的处理器,每个处理器都能够独立运行。在最广义的意义上,通过调制解调器或网络连接的任何两台个人电脑都可以被称为松散耦合多处理器系统;然而,该术语更常用于更密切连接的系统,例如通过先进先出(FIFO)缓冲区、双口RAM或专用串行总线进行连接。大多数这样的系统在运行时之前将任务划分得很好,以使每个处理器可以在任务的专用部分上达到其最佳能力。处理器之间的类型通常是不同的(例如,数字信号处理器和通用处理器)。
在本书中我们将深入讨论的多处理器系统类型被称为紧密耦合多处理器(图1.12b)。在紧密耦合系统或共享内存机器(SMM)中,通过系统总线,两个或更多处理器访问单个主内存。任务分配是由软件在运行时进行的,可以完全灵活地运行各种应用程序。目前,紧密耦合架构主要用于文件服务器、超小型计算机和大型机,但当前的趋势表明它们可能成为下一个世纪普遍采用的架构。
紧密耦合系统需要缓存的原因并不难理解。在紧密耦合系统中,每个处理器都有自己的缓存(图1.12b)。缓存的访问频率越高,处理器使用总线访问主内存的次数就越少。这有助于防止总线饱和,即降低利用率,使得总线及时对那些需要的处理器更加可用。
缓存处理器所需的总线带宽与缓存的缺失率成正比,因此设计多处理器系统的设计者比单处理器系统的设计者更注重缺失率。单处理器系统的设计者可能会犹豫是否要花费额外的资金将缓存的命中率从96%提高到98%。这对性能测试的影响几乎可以忽略不计。然而,在多处理器系统中,命中率从96%提高到98%意味着缺失率从4%降低到2%,即减少一半。换句话说,一个设计所需的带宽是另一个设计的一半,因此在饱和效应开始显现之前,可以使用系统中只有一半数量的处理器。
让我们以更直观的方式来分析多处理器系统中加入处理器的情况会发生什么。我们的理想是获得与加入系统的处理器数量成比例的增量性能增加。一个五处理器系统应该以单处理器系统的五倍或接近于五倍的性能运行。为了这个论点,让我们假设示例中使用的处理器每条指令需要两个时钟周期,所有指令需要单个存储器访问(一个不真实的低数字),并且主存储器的延迟时间为零。

在单处理器系统中,使用了一半的主存带宽,并且我们可以实现一个百分之百的标准化性能水平。如果我们添加另一个处理器,并且这两个处理器交错,它们将消耗整个总线带宽,并且将以单处理器系统的200%的速度执行。在这样的系统中添加第三个处理器将不会增加任何东西,因为它没有交错插槽,并且没有内存带宽可以服务于第三个处理器。这个系统的性能与处理器数量的关系如图1.13所示。
这种情况过于简化了,但它为更现实的问题奠定了基础。通常,复杂指令集计算机(CISC)的指令需要可变数量的周期,并且每个指令使用多个主存访问。这立即排除了交错的有效性,因为它需要平衡和可预测才能有用。在实际系统中,总线仲裁被用于允许所有处理器根据需要访问总线。仅仅使用仲裁机制会减慢速度,因为在分配前处理器必须请求总线。这意味着主存访问直到处理器尝试启动访问一次仲裁延迟后才能开始。仲裁的效果是单处理器不再能够以上面示例中的100%的水平执行,并且添加处理器对系统的整体性能有一个更渐进而不那么有用的影响。

在图1.14a中,我们看到当处理器被添加时,仲裁多处理器系统的操作方式。添加处理器增加了同时仲裁请求的概率,所以添加处理器的性能增加并不像第一个例子中那么显著,当接近总线饱和时,性能会有更加渐进式的下降。在这样的系统中,当达到饱和时,添加处理器实际上会减慢整个系统的速度。从这个角度来看,如果每个添加的处理器需要恰好20%的可用总线带宽(一个不真实的低水平),那么一个六处理器系统将需要120%的可用带宽,将以100%/120%的速度运行。高性能处理器实际上需要非常接近100%的总线带宽,所以问题比这个例子显示的更糟。
在我们的例子中仍然使用零等待主存储器。当我们为这个维度添加一些现实情况时会发生什么呢?很容易看出,我们可以将通常是单周期主存访问加入一个等待状态,并将总线带宽减半,因此在前面的段落中使用的100%数字降至50%。每个处理器现在只能以其潜力的50%/120%操作。六处理器系统的标准化吞吐量降至6 x 50%/120%,或单处理器与零等待主存储器系统的吞吐量的2.5倍。如果每个处理器的总线利用率被提高到更真实的80%水平,那么六处理器系统的性能将为6 x 50%/480%,导致组合系统吞吐量只有单个零等待处理器的63%!因此,在实际世界中,未缓存的紧密耦合多处理器不是提高系统吞吐量的一种具有成本效益的方法。当我们添加缓存时会发生什么呢(图1.14b)?回到交错示例。假设我们拥有与第一个示例中相同的零等待状态系统,并且每个处理器都有一个非常适度的90%总体命中率的高速缓存,或者是10%的缺失率。突然间,50%的总线使用要求降至50%的10%,即5%。这意味着总线现在不会饱和,直到安装了20个处理器!多么不同啊!当然,这都是在一个非常理想的环境中进行的,在这个环境中,处理器只需要50%的总线带宽,主存储器没有等待状态。我们刚才看到,等待状态对统计数据产生全新的影响,大多数处理器超过80%的时间需要其总线。向多处理器系统添加缓存的一个不幸的副作用是,设计者现在必须考虑所有可能的新问题,因为两个缓存可能包含相同的数据,并且这些多个副本不会自动匹配。这是一个非常困难的问题,整个第四章都致力于解决它!简言之,向多处理器系统添加高速缓存的好处足以使人们了解多处理器缓存协议的令人昏昏欲睡的练习。我曾经看到一种奇怪的扭曲方式,用于减少多处理器系统中的总线流量,它涉及由动态RAM构建的缓存。缓存越大,错失率越低。如果流量是您的问题,则错失率是您的解决方案。DRAM每美元给您提供的内存是SRAM的四倍,但速度较慢。这个缓存的设计者非常关心总线流量,而不是单个处理器的性能,因此选择制作一个更大的、虽然速度较慢的DRAM缓存,而不是制作一个速度更快的SRAM缓存。
1.9 REDUCING POWER CONSUMPTION
最近出现了一种使用缓存存储器的新理由,这已成为一些争论的主题。有人认为可以将缓存存储器添加到低功耗系统(如笔记本电脑)中,从而降低整体功耗。反对者立即指出,实现缓存设计通常使用的快速存储器和逻辑的功耗很高。如何在不增加功耗的情况下将它们添加到系统中?此外,他们经常引用统计数据,显示笔记本电脑的主要功耗点是平板显示器的背光和硬盘驱动器,它们共同消耗了系统功耗的三分之二以上。
支持者认为,缓存降低了主存储器的访问次数,通常使其几乎等于允许的最小刷新周期的数量。这本身大大降低了DRAM的功耗,相比每个存储器周期都访问DRAM时的功耗。该功耗减少几乎抵消了相同数量的SRAM的功耗。此外,如果任何时候访问的DRAM数量大于为缓存打开的SRAM数量,则处理器从缓存运行时的功耗必然低于从主内存运行时的功耗。最后,CPU在其关闭周期之间遭受的较少等待状态,CPU的整体功耗将降低。
//所以,若使得cache miss率降低后,要注意CPU+DRAM的功耗减少量和SRAM的功耗增加量
关于这一点,还没有定论。我个人怀疑,缓存作为节能设备的优势如果存在,也很小,并且高度依赖软件。想象一下,一个系统制造商自豪地宣称某个型号的电池续航时间在运行Lotus 1-2-3时通常为三小时,但在运行Microsoft Word时可以达到五个小时。
在某些手持系统中,处理器的内部缓存可以与定制软件结合使用以降低功耗。首先,将软件划分成模块,以适应缓存的大小,使整个模块在需要时驻留在缓存中。当该模块被加载到缓存中时,主存储器(有时为ROM或其他非易失性存储器)以正常功耗运行。当该模块在缓存中执行时,主存储器不需要供电,因此可以进入降低功耗模式,或完全关闭。通过利用缓存,可以降低功耗,并且如果软件经过适当调整以最小化缓存访问,则可以将功耗最小化。
1.10 AN EXAMPLE CACHE
图1.15展示了一个示例缓存,以确保你明白缓存设计并不难。该缓存是为68020设计的一款旧缓存,它是一款32位的摩托罗拉处理器,没有内置缓存。该缓存使用离散逻辑而不是可编程逻辑设计,这对我们来说是个优势,因为这样设计更容易理解。

图1.15a展示了两个存储器阵列,左边是缓存标签RAM,右边是缓存数据RAM。使用更现代的部件,整个缓存数据RAM可以仅用四分之一的32Kx32 SRAM芯片实现。缓存标签RAM使用了集成复位可重置的缓存标签SRAM芯片,这些设备将在第二章详细介绍。从根本上讲,这些芯片与标准的8Kx8芯片之间几乎没有区别。

//cache controller
//包含:HIT/MISS LOGIC, CACHE ENABLE/DISABLE,WRITE PULSE GENERATION
移动到图1.15b,我们可以看到实现整个缓存所需的所有逻辑。这包括六个存储元素、16个双输入门和五个反相器。地址解码器U10仅用于将地址映射为软件缓存复位命令。缓存策略的名称实际上比这些逻辑少得多,但可能更令人生畏。这个缓存是一个直接映射的写通透统一逻辑缓存。如你所见,名称比逻辑本身更复杂。
尽管逻辑看起来很简单,但不要被误导!有很多方法一开始看起来非常简单,只需要很少的逻辑就可以实现,但它们与系统的其他部分存在许多相互依赖关系,使得问题很快变得复杂,让许多设计者无法在第一时间完成缓存设计。Chips & Technologies在M/PAX芯片组中由于缓存策略的延迟问题受到了重创,而在英特尔将数百万个奔腾处理器交付之前,他们才解决了多处理缓存协议中的所有缺陷。
Chapter2 HOW ARE CACHES DESIGNED?
在本章中,我们将研究缓存设计中使用的一些方法或"行业诀窍"。虽然没有特别复杂的设计技术被使用,但已经投入了大量思考来揭示显而易见的东西,典型的设计师最终将不得不记下许多针对缓存的经验法则,以便正确地进行缓存设计。
2.1 THE CPU-TO-MAIN-MEMORY INTERFACE
正如第一章所讨论的,CPU缓存实际上插入在CPU和系统主存储器之间。正因为如此,为了让缓存欺骗CPU,使其误以为缓存访问实际上是对主存储器的访问,并且没有其他操作,缓存必须匹配CPU和主存储器之间的接口。
2.1.1 Why Main Memory Is Too Slow for Modern CPUs
看起来计算机系统的所有部分都在同时加快速度,因此系统设计师可能会得出这样的结论:任何增加CPU时钟速度的提升都将被主存储器速度的相应提高所匹配,从而使系统吞吐量可以通过简单地组合一个速度提高了50%的主存储器和一个速度提高了50%的CPU来增加50%。同样的半导体加工技术升级使处理器能够处理更高时钟频率的速度提升,也应该能够在内存速度上产生类似的提升,所以应该没有问题。

然而,这种思维方式的错误在于过于简化了问题。从图2.1中,我们可以清楚地看到有一些逻辑元素将CPU与主存储器隔离开来。这些设备的例子包括在DMA访问或DRAM刷新周期期间隔离CPU与主存储器所需的缓冲区。大多数系统中也需要缓冲区,仅仅因为CPU不是设计用来支持其输出引脚上的重负载。微处理器的时序和输出电流通常规定为比其他十个或更多集成电路的输入电容总和要小得多的负载,因此地址输出缓冲区成为必需品。
现在,每个缓冲区都会增加延迟,而这个延迟并不随着类似的半导体工艺技术中内存或CPU速度的增加而很好地扩展。一个技术中的5ns门延迟在下一代工艺中可能会损失0.5到1ns。部分原因是仅将输出引脚上下移动需要一定的时间。对于TTL I/O电平,这个过程大约需要3ns的时间。随着CPU变得更快,缓冲区延迟会占用主存储器允许的周期时间的越来越大的比例。
其他与工艺不成比例且进一步恶化问题的定时规格包括数据设置时间和数据保持时间。虽然微处理器制造商通常会稍微降低产品的设置时间以实现更快的速度等级,但这种减少与时钟频率的变化绝非成比例,而且常常在更快的CPU时钟频率上遇到瓶颈,即在最快速度等级与下一个较低速度等级之间没有变化。

图2.2说明了这些不太灵活的参数如何对主存储器速度施加压力。如果继续推到极端情况,一个足够快的处理器将需要具有负访问时间的主存储器,以便该处理器能以最快的吞吐量运行。到目前为止,动态RAM还没有显示出预测未来的能力。
2.1.2 How the CPU Handles System Bus Delays
现代微处理器几乎都遵循类似的总线协议。仅在最先进的处理器接口中才使用分裂事务。在分裂事务中,处理器发出一个命令,然后从总线中移除自己,直到内存或I/O设备发出响应。在不使用分裂事务协议的系统中,一旦地址被发出,处理器就控制总线并等待事务终止。如果另一个总线主控覆盖了这个事务,则处理器重新获得总线控制权后整个过程将重新启动。
微处理器使用输入信号确定从主存储器返回的数据是否稳定并且有效。然后允许微处理器终止周期。这个信号的名称因处理器而异,但通常被称为Ready信号。在本书中,即使是使用不同命名约定的处理器,我们也会使用"Ready"这个全局术语。

//注意CPU端的Ready Input信号
Ready输入由一个响应时间设置为与主存储器或处理器控制的任何其他总线设备(即I/O设备)的访问时间匹配的设备产生。在速度较慢的系统中,CPU允许所有设备有足够的时间来响应。这尤其适用于时钟频率为1到10 MHz的处理器。在所有总线设备的响应速度都与处理器要求的速度一样快的系统中,Ready输入通常被硬连到断言状态,表示数据将始终准备好以响应CPU的请求。需要CPU根据内存或I/O设备进行节奏控制的系统可能会使用单稳态多谐振器(一次性触发器)、短移位寄存器或计数器来生成不同速度内存或I/O设备的不同长度延迟(见图2.3)。这些定时发生器的延迟时间由处理器对内存或I/O设备的命令触发,该命令告诉该设备启动总线事务。定时发生器设置为匹配或超过内存或I/O设备要求完成总线事务所需的时间。高度调整的系统将为系统中的每个不同延迟使用一个定时发生器。速度不太关键的应用程序将为所有响应时间使用单个定时发生器,并仅将延迟设置为与总线上最慢的设备匹配。
如果CPU采样后Ready输入响应说总线数据还没有准备好,CPU将等待一个时钟周期,然后再次采样信号。所有处理器输出信号从期望Ready输入的时间开始直到被断言之间都是静态的。高速缓存设计者意识到其系统无法在没有等待状态的情况下实际运行,因此他们试图将给定处理器时钟频率的等待状态数量最小化。
//CPU处理总线延迟时有个Ready信号,怎么解释?
/*
"Ready"信号是指处理器和外部设备之间用于协调数据传输的一种控制信号。当处理器需要从外部设备(如内存或I/O设备)读取数据时,它会向外部设备发送一个请求,并等待外部设备发出"Ready"信号,表示数据已经准备好可以传输。
具体来说,当处理器发出一个读取操作的请求时,外部设备会开始准备数据,并在数据准备就绪后通过"Ready"信号通知处理器。处理器在接收到"Ready"信号后,才会实际读取数据。这样可以确保在进行数据传输时处理器和外部设备之间的协调和同步。
"Ready"信号的作用是避免处理器在数据未准备好的情况下进行读取操作,从而防止数据错误或不完整的传输。它也有助于处理器在面对不同速度的外部设备时进行合理的协调和等待,以保证数据传输的正确性和稳定性。
总的来说,"Ready"信号在处理器处理总线延迟时起到了协调和同步的作用,确保数据传输的顺利进行。
*/
2.1.3 The Cache's Interface to the CPU
高速缓存必须受CPU控制。如果高速缓存在CPU读取周期中包含所请求的数据的有效副本,则高速缓存允许CPU以高速缓存的速度进行操作。如果高速缓存不包含副本,则高速缓存启动主存储器读取周期,复制由主存储器提供的数据(通过来自主存储器对CPU的Ready输出指示为有效),并允许CPU继续进行。传输给CPU的数据由缓存控制器从高速缓存(在缓存命中情况下)或缓冲区路由到CPU总线与高速缓存相隔离的主存储器中(在缓存未命中情况下)。高速缓存必须拦截CPU的全部信号,包括输入和输出,并确定这些信号是否需要路由到/来自主存储器,或者它们应该保留在高速缓存本地。在某种程度上,高速缓存是CPU与外界之间的绝缘层。


有四种基本的高速缓存与CPU的交互方式,所有这些方式都由高速缓存控制器来控制:读命中(read hit)、读未命中(read miss)、写命中(write hit)和写未命中(write miss)。这些在图2.4中有所说明。在高速缓存读命中中,缓冲区被关闭,将CPU/高速缓存子系统与计算机的其他部分隔离,并且高速缓存控制器向CPU生成就绪信号。CPU/高速缓存子系统与主存储器总线之间不需要相互作用,某些系统利用这一点,允许DMA或其他设备在CPU从高速缓存操作时控制主存储器。偶尔,设计师会允许CPU地址在每个周期的开始阶段通过地址缓冲器传播到总线上,无论该周期是读命中还是读未命中。高速缓存的设计方式是在高速缓存访问同时启动主存储器访问,而不是等待高速缓存未命中再开始主存储器访问。这种方法可以缩短高速缓存未命中时的主存储器访问时间,并提高单处理器、单任务系统的运行效果,但对于多任务系统和多处理器系统可能会有不利影响,因为高速缓存会让CPU浪费大量的主存储器总线带宽。这种高速缓存设计可以称为旁观式设计(look aside designs)。由于处理器随时可以选择访问高速缓存或主存储器来请求数据,旁观式高速缓存可以作为计算机系统的附加组件。
将自己置于CPU和主存储器交互的核心位置,干预所有的CPU和主存储器事务的高速缓存被称为透明式(look through)或内联(inline)高速缓存。透明式高速缓存首先在高速缓存中查找要访问的位置。只有在检测到未命中时,高速缓存才能触发主存储器周期的开始。这样做有利有弊。好处是透明式高速缓存极大地减少了主存储器总线的通信量。这在其他处理器必须访问主存储器总线的系统中非常重要。不足之处在于,由于高速缓存未命中导致的所有主存储器访问现在都延长了时间,以确定是否发生了高速缓存未命中。幸运的是,这种情况并不经常发生,并且由于高速缓存被设计为尽快响应,延迟并不大。然而,这足以引起重视,因此被称为查找惩罚(lookup penalty)。
读取未命中周期指当CPU输出的地址与目录内容不匹配时,处理方法恰好相反。缓存输出关闭,地址和数据缓冲区打开,允许主存数据输入到CPU。缓存控制器的Ready信号也关闭,系统的Ready则直接发送给CPU。当CPU读取主存数据时,缓存控制器命令缓存数据RAM进行复制,并命令缓存标记RAM复制CPU地址输出的标记位,从而覆盖先前驻留在相同集合地址处的任何已存在的缓存数据行和目录地址。这个"从主存中读取/写入缓存"的循环称为线更新、更新、拷贝-进、获取、线填充或线替换周期。为了减少混淆,我们将尽量使用"线填充"(虽然在第2.2.5节之前,我们不会具体定义"线"的含义,但现在我们将使用宽泛的定义,即与单个集合地址对应的缓存条目是缓存行)。新数据现在可以从缓存中获取,在随后的多个缓存命中循环中很可能会被访问数次,直到它也被覆盖为止。这段文字描述了缓存存储器设计背后的重大概念,因此如果感觉没掌握好,就不要跳过去了。
和在第1.6节中定义的强制未命中一样,有些情况下会存在强制线填充,即即使使用更好的缓存策略也无法避免的线填充。
写命中周期分为两种处理方式。这两种方式足够复杂,需要另外一节来全面解释,因此在第2.2.4节中详细介绍这两种策略。在图2.4c中的示例中,匹配的缓存行和主存均用新数据进行更新。
写未命中周期则多种处理方式并存。在像图2.4d中的缓存中,写未命中被忽略并直接传递到主存。在其他情况下,写数据会在写入主存的同时覆盖缓存中的一行。处理写未命中的第三种方法是覆盖缓存中的一行,并禁止将写周期复制到主存。在第2.2.4节中,我将尝试证明这些方法和其他方法的有效性,并说明哪些方法适合于哪种写策略。
曾经为不明确的重启序列问题而烦恼的设计师可能已经想知道,在冷启动后缓存如何启动。显然,缓存数据RAM中的所有数据以及缓存目录中的所有地址都是完全随机的。设计师如何避免将这些随机数据误认为是好的数据,并生成错误的缓存命中周期?其实有两种简单的处理方法。最简单的方法是禁止缓存向处理器提供数据,直到引导程序(通常在可编程只读存储器[PROM]中)有机会覆写所有缓存数据和标记地址。一个一位硬件复位标志输入到缓存控制器中,表示整个缓存都将被忽略。引导程序从引导PROM本身读取一个大小与缓存大小相当的内存块,每次读取循环都将被缓存控制器视为读取未命中周期,因此缓存控制器将在缓存中写入引导的新副本。一旦整个缓存已经用这些新数据覆盖,通过软件设置一位标志,缓存控制器现在允许缓存向处理器提供零等待数据。

更常见的验证缓存内容有效性方法是为缓存中的每一行提供一个有效状态指示器。尽管通常通过专用的有效位来表示有效性,但在第4章详细介绍的某些更复杂的一致性协议允许每个缓存行存在四到五种状态,其中有效状态是编码在两种或三种状态位之一的许多状态之一。图2.5显示了使用有效位的一种系统中缓存标记RAM和相应的缓存数据RAM的内存组织。本节稍后将详细介绍该特定布局的替代方案。
还存在一些问题,即如何确保每行的有效位在冷启动后重置为无效状态。这个问题有两个简单的答案。首先,对于每一行具有一个有效位的缓存,可以复制刚才描述的缓存无效标志,其中缓存将被关闭,直到所有有效位都有机会被验证或无效。这可能似乎没有意义,但我们将在第4章中看到,如果有效状态是更复杂的行状态方案的一部分,则这种方案可能会证明是有益的。其次,在冷启动后可以重置所有有效位。执行此操作有三种简单的方法。一种方法是购买具有复位功能的专用静态RAM。图1.15中的应用程序使用了这种方法。另一种方法是使用标准静态RAM来保存有效位,并使用小状态机遍历所有地址,在系统重置后将所有地址的有效位写为无效状态,同时使处理器处于空闲状态。某些CPU内部执行此类型的重置序列。最后一种方法与之前描述的方法非常相似,在启用缓存之前将所有缓存位置设置为有效状态,但是,对于这种最后一种方法,在缓存处于禁用状态时,将整个缓存写入无效状态。该方法与本段开头描述的方法之间的差异微乎其微,可能只取决于引导程序是否可缓存。

有一种极其简单的生成有效位的方法,许多商用缓存设计都在使用。缓存标记RAM使用比较器和标准静态RAM构造。某些集成缓存标记RAM包含比较器和可复位静态RAM(图2.6)。这些RAM上的复位引脚将整个存储器阵列中的每个位清除为零。诀窍是将这些设备上的任何多余数据位输入连接到逻辑高电平,以便在比较周期中,这些输入将与零进行比较,表示自复位以来尚未写入任何标记地址到RAM中,或者与1进行比较,表示确实发生了写入周期,并且这些数据输入的高电平已写入所选RAM集地址。
2.2 CHOOSING CACHE POLICIES
缓存策略有很多种,本书只涉及最常见的几种。缓存策略是缓存的操作规则。哪些周期将从缓存中读取,而不是从主存中读取?缓存在系统中的位置是什么?缓存的联想性如何?写入周期期间会发生什么?在开始设计之前,所有这些问题必须针对所有情况进行回答。
选择缓存策略是为了达到最低成本的最高性能。在这个方程中有两个变量:1)哪个更重要,节省工程时间还是节省整体系统部件成本?2)缓存是要集成还是由离散组件构建?
阅读本章后,您应该会得出这样的印象:某些缓存设计非常简单,可以在很短的时间内完成。另一方面,如果设计人员花费近乎无限的时间来开发,可以从缓存中提取出最后一丝性能。另外,我将在适当的情况下指出,某些体系结构与标准RAM组织不太兼容。在少数情况下,静态RAM制造商已经解决了这个问题,但在大多数情况下,更复杂的体系结构最好通过专用的单片缓存设计来处理。处理器芯片上的内部缓存的设计者已经探索了这条路线。
在尝试产生最佳缓存设计时,一个问题是优化具有约20个变量的方程。我们将在本章中探讨这些变量。
根据系统的普遍性和改进设计所需的资源量,可以以多种方式选择缓存策略。在最好的情况下,系统的硬件和软件是同时设计的,软件中包含的情况很少,因此可以基于大量关于不同缓存策略对软件性能影响的经验研究,将硬件优化到非常好的程度。在最坏的情况下,硬件设计人员被要求在没有关于系统上运行的软件的任何知识、实证数据或开发的机会,以及对各种缓存策略权衡的很少了解的情况下设计缓存。现实生活中的情景往往遵循生产该系统的公司的财力强弱以及系统的开放性。在盈利性强的企业内部设计的封闭系统通常会遵循最好的情况,而在为了在开放的系统市场上竞争而设计系统的盈利性较低的企业将不得不忍受与最坏情况非常相似的场景。
以下示例将帮助那些无法进行大量实证研究的设计人员,并向刚开始学习缓存设计的人说明各种策略之间的权衡。
2.2.1 logical vs. Physical

在使用虚拟寻址的系统中,缓存可以位于处理器的内存管理单元(MMU)的上游(CPU侧)或下游(主存储器侧)。图2.7展示了两种配置。MMU上游的地址都是逻辑或虚拟地址,下游的地址是物理地址。如果缓存位于MMU上游,则称为逻辑缓存或虚拟缓存;如果缓存位于MMU下游,则称为物理缓存。这两种放置方式都有利弊。
由于逻辑缓存在延迟引起设备(MMU)的上游,因此逻辑设计比物理设计运行得更快。图1.15展示了Motorola 68020 CPU的逻辑缓存。68020使用单独的芯片作为其MMU,而图1.15的缓存则放置在MMU芯片的处理器侧。
逻辑缓存会出现一种称为地址别名、简称别名或同义词的现象。在虚拟内存系统中,同一物理地址可能映射到两个或更多完全不同的逻辑地址。假设这两个逻辑地址都被缓存,并且对其中一个进行了写入操作。缓存将更新缓存的物理地址副本以及主存储器本身(允许策略),但另一个逻辑地址的缓存副本将保持不变,并随后包含错误数据。这个问题有几种解决方案,在4.2.2节中将详细记录其中一种,但这些解决方案并不是微不足道的。
2.2.2 Associativity
在第1.5节中,我们看到了集合关联缓存和完全关联缓存之间的区别。在完全关联缓存中,目录中每一行都有一个比较器,并且所有行都会同时进行匹配检查。而在集合关联缓存中,只使用一个比较器,并且将目录划分为集合位和标记位,其中集合位确定数据在缓存中的具体位置。由于这种限制,集合关联设计中的两个缓存行不能使用相同的较低地址或集合位。这可能导致抖动(thrashing),即两个地址不断互相替换在缓存中,以牺牲吞吐量为代价。你可能还记得,抖动不仅发生在指令之间互相干扰时,还会在某些内容被推入栈位置、读取或写入数据时发生,当这些操作的集合位恰好与有效的缓存位置的集合位匹配时。
在完全关联设计和集合关联设计之间存在一种中间地带。通过向简单的集合关联缓存设计中添加比较器,可以实现更高程度的关联性。通过向缓存设计中添加一个关联度,就可以引入另一个用于映射具有共享集合位的主存储器地址的位置。如果在单比较器设计中两个位置通常会互相抖动,那么在具有第二关联度的设计中它们就不再需要抖动。第1.6节中的缓存使用了单个比较器、单个缓存标记RAM和单个缓存数据RAM,具有单一关联度。这是设计缓存的最简单、最常见的方法,称为直接映射实现方式。

在较小的缓存中,通过实现更高程度的关联性或在缓存中增加更多路(有时称为bank),可以实现显著的命中率改进。缓存中的每一路都相当于另一个缓存,它们几乎完全一样(图2.8)。直接映射缓存由缓存数据RAM、缓存标记RAM(由一个RAM和一个比较器构建)、缓存控制器和隔离缓冲区组成;而N路组相联缓存使用N个缓存数据RAM和N个缓存标记RAM(由N个RAM和N个比较器构建)、缓存控制器和隔离缓冲区。在组相联缓存中,主存储器地址可以被映射到与路数相同的不同位置。(一路缓存总是称为直接映射缓存。)
下面是一个例子:针对一个16位微处理器,可以使用两个8K x 8 SRAM来设计一个16K字节的直接映射缓存,还可能需要一个更多的8K x 8位SRAM和比较器来实现缓存标记RAM(具体取决于系统中使用的地址位数,在这个例子中是20位)。要为同一系统构建一个二路16K字节的缓存,需要四个4K x 8位数据RAM,并且需要两个4K x 9位RAM和比较器来实现标记。
之所以在二路设计中有更多的标记位,是因为每个缓存都是独立的,所以它们必须像是系统中唯一的8K字节缓存一样工作。你可以从这个简单的例子中得出一个趋势。对于给定的缓存大小,增加关联性会减少缓存的深度,从而减少集合位的数量。被替代的位必须转换为新的标记位,因此缓存标记RAM和比较器必须变宽。在我们假设的地址位数(20位)下,16K字节的缓存需要使用13个集合位、七个标记位和一个有效位,而8K字节的缓存则需要12个集合位、八个标记位和一个有效位。以类似的方式,四路16K字节的缓存需要11个集合位和九个标记位。如果将此推到极端,就回到了图1.7中的内容可寻址存储器。每个缓存数据RAM已经缩减到单个位置,完全关联缓存中有2^N个缓存数据RAM。同样地,有2^S个单独的缓存标记RAM(和比较器),每个只存储一个地址。集合位的数量已经减少到零,地址现在完全由标记位组成。在完全关联缓存中,每个缓存行都是不同的路。
在N路缓存中,所有的集合位同时被发送到每个路的缓存标记RAM和缓存数据RAM,因此最终决定使用哪个路的缓存数据RAM取决于当该路的缓存标记RAM指示命中发生时,启用适当的路的缓存数据RAM的数据输出引脚。在这个领域中,一个不幸的受害者是术语的混淆。对于MMU来说,一个页面是由翻译后的位引用的主存储器部分。这是我们在前面一节中使用的术语。对于某些缓存设计师来说,"页面"一词指的是关联缓存中的一个路,而对其他人来说,"页面"意味着缓存中的一个单独条目。在本书中,"页面"只在MMU的意义上使用。
在第一节中我简要提到了英特尔的一个类比,将缓存比作冰箱,对于有限部分的食品,从家中可以更方便地访问冰箱,而不是杂货店。在同样的类比中,英特尔巧妙地将关联性比作冰箱中的货架,它们既不增加也不减少冰箱本身的空间,但减少了放置冰箱内容的位置竞争,从而增加了您能够放置更多内容的可能性。如果我们回顾一下文件系统类比,更高的关联性可以看作是向书桌上添加文件抽屉。

随着缓存大小的增加,通过增加关联性实现的命中率改进会迅速减少(参见图2.9)。在这个例子中,一旦缓存超过一定大小(约4K字节),将缓存大小加倍比增加给定大小缓存的关联性能更好地提高命中率。尽管通过使用更高程度的关联性可以获得略微更好的32K字节缓存的命中率,但要实现这样一个缓存所需的组件数量将与关联性的级数成比例增加,并且可能不足以对抗64K字节实现的优势。这是因为每个缓存路都需要一个单独的缓存标记RAM和一个单独的缓存数据RAM。这是离散缓存设计中的一个关键点,但在集成设计中却不是一个很大的问题,因为集成缓存并不关心芯片数量,而是关心在特定的晶片尺寸内能够达到的最大命中率。
然而,你可能希望记住一些常常引用的统计数据,无论它们与你自己系统的实际表现有多大差异。第一个是,关联性加倍可以将缺失率降低约20%。这来自于M. D. Hill在1987年在加州大学伯克利分校完成的论文"缓存内存和指令缓冲区性能的若干方面"的研究。第二个经验法则是,将缓存大小加倍可以将缺失率降低约69%。这是斯坦福大学的Anant Agarwal的研究成果。从图2.9或类似的图表来看,很难证明这些数字的合理性。然而,事实确实是,在某些缓存大小以上,当有机会时,增加缓存大小要比增加关联性更好。正如以往所说,我只是说你的缓存必须为你的系统和软件而设计。对于除了简单指导之外的其他任何统计数据,你都不能简单的利用。


在设计离散的多路缓存时还存在另一个关键困难。大多数缓存都被设计成支持尽可能高的处理器时钟速率,因此它们会对即使最快的静态RAM和缓存标记RAM的能力造成压力。在直接映射缓存中,缓存标记RAM的关键路径经过静态RAM和地址比较器,到达缓存控制逻辑,最终返回到处理器的就绪输入引脚(图2.10)。在直接映射设计中,缓存数据RAM通常在处理器指示开始读取周期后立即启用到处理器的数据输入引脚,只有在检测到缓存未命中时才会禁用,因此数据RAM的输出使能引脚的时序并不紧密。在多路缓存的离散实现中(图2.11),在检测到缓存命中之前,不同路的数据RAM输出均不能启用。这意味着关键时序路径现在通过缓存标记RAM、比较器和缓存控制器,然后通过缓存数据RAM的输出使能引脚,将"输出使能到有效"数据延迟添加到关键时序路径中。这通常是相当重要的(在本书编写时约为Sns),并且会让设计者远离在设计初期采用离散缓存的多路实现。
需要提及一些关于选择哪个路被替换掉的方法的方法。这个策略被称为替换算法,尽管有些人只是称之为放置。当将新行放入缓存时,替换算法会选择要更新的路(请记住,任何主存储器地址都可以放入与缓存中相同数量的位置)。理想情况下,将要被覆盖的任何过时的缓存数据都不再被处理器使用。一些缓存控制器会监视对缓存的访问,并将每个路的访问顺序进行分类,记下最近被访问最少的路的行。这被称为最近最少使用(LRU)算法。LRU统计数据分别维护每个缓存行。

在两路系统中,可以使用每行的单个位来实现LRU(参见图2.12)。当发生缓存命中时,被命中的路确保将LRU位写入指向另一条路。当需要替换一行时,该行的LRU指针已经指向应该发生替换的路,指针被重写为指向另一条路。这些位的上电状态无关紧要,因为缓存中的所有内容均不可用。
有趣的一点是,真正的LRU替换算法会消耗大量的内存位。可以很容易地观察到,字母表中N个字母可以以N!种方式排序。一个四路缓存必须为每行拥有五个LRU位,以表示缓存内容的24 (41)种可能的使用状态(A、B、C和D的使用顺序):

因为这些24种状态需要五位二进制数(25 = 32 > 24)来编码。 类似地,一个八路缓存的LRU需要足够的位数来表示八种缓存路的8!种使用状态。这相当于40,320种状态,每个缓存行需要16位的LRU信息。一个十六路缓存每个缓存行需要45位,而一个完全关联缓存具有256行(本质上是一个256路组相联内存),需要的LRU位数超过了我的口袋计算器所能表示的范围!

真正的LRU系统设计面临的另一个问题是,如果要更新LRU算法以显示最后四个路的访问顺序,则在每个处理器周期中必须执行读取周期和写入周期来更新LRU位。另一种方式是,如果LRU当前将顺序表示为ABCD,然后访问了D,顺序必须更改为DABC。结束的顺序与初始顺序密不可分。本章的最后几节将专门讨论缓存设计中的时序问题,我们将看到在缓存中执行简单的读取周期已经足够困难,因此读/写配对可能变得不可行。已经尝试了许多真正的LRU算法的替代方案,下面的段落将简要描述其中一些。
英特尔在公司的IntelArchitecture微处理器中使用的方法是一种替代方式,称为"伪LRU"。每个缓存行使用三位,如图2.13所示。树形结构中的顶部位(AB/CD)在A或B命中时设置,并在C或D命中时清除。在图示的第二层中,只有一个位能在缓存命中时设置或清除。如果命中A,A/B位将被设置,c/n位不会发生任何变化。如果命中B,A/B位将被清除,同样地,C/D位不会发生任何变化。对于C/D位,在C或D路命中时也是相同的情况。这种方案可以实现仅写入,即在缓存命中时可以使用简单的写周期来更新伪LRU位,而不需要耗时的读取/修改/写入周期。这实际上可以使缓存的访问速度比真正的LRU算法快一倍。只有在进行行替换时,伪LRU位必须读取,这将是一个较慢的周期,因为它涉及到片外访问。
这种方法与真正的LRU算法之间的差异很小,可以简单地进行说明。假设某个行在A、B、C和D四个路上都包含有效数据。如果CPU执行一个循环,在该行中按照A、B、C、A、B、C的顺序不断访问,那么显然D路是最近最少使用的。然而,在C访问之后,AB/CD位将指向AB,而A/B位将指向A,强制更新数据覆盖A路而不是D路。这种情况发生的可能性非常依赖于软件,但统计上可能很小,并且与整个处理器时钟频率可能需要降低以适应真正的LRU算法的可能性相比,显得微不足道。英特尔的伪LRU还消耗了一半的内存位,因此占用了一半的芯片空间,而这些空间可能已经用于提高处理器的整体吞吐量的其他问题。有趣的一点是,英特尔的这种三位方案需要使用双口SRAM。无论命中哪个路,两位将被指向离最近使用的路最远的位置,第三位将保持先前操作的位置。英特尔保持第三位不变的方法是对这三位进行读取/修改/写入操作,这似乎使得复杂性接近于更直接的方案。英特尔的PC处理器不是按位写入内存,而是在三个LRU位上执行读取/修改/写入操作,导致未修改的位被写回到操作开始时的值。双口SRAM有一个端口专门用于在周期早期读取LRU位,另一个端口专门用于在周期后期将修改后的LRU位写回SRAM。另一种替代方法称为最不经常使用(FRQ)方法,在缓存设计中使用较少,而在内存管理单元中使用较多。这种方法用于控制MMU的CAM内容的替换。它在缓存结构中仍然很有用,应该在这里进行说明。每个缓存行都有自己的指针,指向一个随机的路。在一个四路缓存中,每行将有一个指向A路、B路、C路或D路的两位指针。在这种方案中,当访问一行时,还会访问指针。如果指针当前指向生成缓存命中的路,缓存控制器将使指针递增,以便最终指向下一个路。在连续的对该行的缓存命中周期中,每次发现指针指向正在访问的路时,指针都会递增,直到停在不再生成任何缓存命中的路上。因此,在缓存错误周期中访问该行时,指针将指向最合适的替换路。在替换周期中,指针再次递增,以确保最近更新的数据不会立即被覆盖。
虽然最不经常使用(LFU)方法简单而优雅,但每次缓存命中都需要进行读取和写入周期。这与真正的LRU算法一样会减慢速度。这意味着使用这种方法的唯一优势是实现所需的总位数比之前计算出的实现真正的LRU所需的位数要少得多。
最受欢迎的替代方案之一是随机替换算法。不需要解释即可理解该算法的基本原理:选择要替换的路线是随机的。实现简单,特别是如果设计者不关心替换的随机性有多高。几乎可以从许多地方以几乎没有成本地获得相对随机的数字。
当然,在一个四路随机替换缓存中,缓存控制器具有四分之一的机会覆盖最近使用的路线,但这仍然优于在直接映射缓存中覆盖混乱位置的100%机会。随机替换算法的一个重要优点是,对于一个四路缓存,真正的LRU每行需要消耗六位,英特尔的方法需要消耗三位,指针方法仅需要两位,而随机替换则不需要任何位来实现。
在最不经常使用方法和随机替换之间,存在一种称为非最后使用(NLU)的方法。与FRQ类似,NLU也使用一个指针,但该指针指向最近使用的路线,并且只是存储了任何特定集合地址的最后命中的路线编号。每个缓存标签RAM的匹配输出简单地被捕获在一个与缓存标签RAM一样深的RAM中。这可以在零时延的写入周期内完成。NLU替换算法的思想是随机替换是可行的,但如果能避免在任何集合地址上随机覆盖最近使用的路线,那就更好了。由于对于两路缓存使用真正的LRU没有任何惩罚,所以该方法只适用于多于两路的缓存,此时在一个N路缓存中,随机替换算法覆盖第二个最近使用的路线的可能性为(1 - 1/N)。NLU可能比纯随机替换稍好,但很难想象它在缓存性能方面提供了显著的改进。
设计并不要求使用以2为底的路数。虽然最常见的关联度为1(直接映射)、2和4,但可以根据系统最佳方式增加或减少关联度。Sun Microsystems在一个处理器设计中使用了五路内部指令缓存和四路内部数据缓存。但我们正在超前讨论。分离的指令/数据缓存将在下一节中讨论。
像图1.4中的示例代码可能最适合四路设计,因为它在数据表、堆栈、调用例程和子例程中使用相同的集合地址。可以自由地想象一下图1.4中程序的追踪情况,然后看看它在直接映射的两路和四路缓存中的行为如何。我决定不逐步说明,在每个步骤中显示写入缓存的内容,因为这将消耗比图1.11中显示的示例所占空间多出数倍。
2.2.3 Unified vs. Split Caches
一种在不引起问题的情况下实现双路缓存部分优势的方法是将缓存分为两部分,一部分用于指令,称为指令缓存,另一部分用于数据,称为数据缓存。这被称为分离缓存架构,而另一种选择是统一或统一指令-数据缓存。到目前为止,所有讨论都假设缓存是统一设计的。如果您看一下图1.4中给出的示例代码,分离缓存的优势立即显而易见。当数据访问与代码访问冲突时,代码会有时候出现问题。如果在图1.11的示例中使用的是分离缓存,步骤5和7中的堆栈推入/弹出操作将不会与其他步骤产生冲突,并且程序计数器的副本仍将存在于子例程末尾的返回指令中。在分离缓存中,对数据空间以及堆栈的访问将通过数据缓存进行,而指令将通过指令缓存进行访问,减少了类似于使用双路架构时的抖动程度。缺点是其中一个缓存可能比另一个缓存填满得更快,并且没有办法让完整的缓存获取用于未使用行的对面缓存中的访问。最糟糕的情况是指令缓存会严重抖动,而数据缓存很少被访问,或者反之。任何一种情况都极大地依赖于软件。
分离缓存最具有优势的特点可能是其固有的简单性。与双路缓存相比,分离缓存的构造更简单,原因有几个。首先,每一侧都可以设计成一个简单的直接映射缓存,完全独立于对缓存的另一半的考虑。其次,大多数处理器通过一个引脚指示当前的读取周期是指令获取还是数据获取,该引脚在地址输出后立即变为有效。这意味着适当数据RAM的输出可以在周期开始时启用,就像在直接映射统一设计中一样,不仅放松了对缓存标记RAM的时序限制,还放松了对缓存数据RAM和控制逻辑的时序限制。一些分离缓存架构直接将处理器的指令/数据输出视为最高有效的集合地址位。第三,在分离缓存中,不存在关于要在哪个路上替换一行的决策。指令会自动放入指令缓存中,数据会自动放入数据缓存中。第四,两个缓存不必大小相等。在大多数常见的计算应用程序中,指令缓存应该比数据缓存大,但是在某些需要大量数据运算的应用程序中,小型指令缓存就足够了,而大型数据缓存则是必需的。另一个不寻常的好处是两个缓存可以使用完全不同的替换策略。数据缓存可以采用高度关联的设计,而指令缓存可以采用直接映射的设计。如果您选择实现分离缓存架构,请先浏览本书以帮助确定指令缓存的一组策略,然后再为数据缓存进行相同的操作。您的设计可能看起来不传统,但更有可能胜过竞争对手。您可以探索不同的缓存划分方式。大多数处理器不仅可以指示当前访问是指令还是数据,而且还会披露用户/特权模式以及请求是否为堆栈操作。通过使用任何或所有这些状态信号来启用不同的分离缓存,您可能能够实现多路缓存的许多优势,而不会遇到速度方面的困扰。
2.2.4 Write-through vs. Copy-back
早在第2.1.3节中,我就延迟了解释缓存在写周期期间采取的操作。这是因为缓存中有几种处理写周期的方式或写策略,并且决策会影响缓存的成本和复杂性。即使在本节中,我也将在第4章之前推迟解释一些涉及写周期的更具体问题。
在写周期期间定义缓存行为的两种基本策略是:写直通(write-through)或存储直通(ST)缓存,以及复制回写(copy-back)、延迟写入、非直通写或存储内(store-in)缓存(SIC)。鉴于所有这些选项,本书将专门使用写直通和复制回写这两个术语,因为它们都很常见且不容易混淆。当然,一旦写策略在硬件中实现,它就成为了写入策略。
在写直通缓存设计中,可以采取两种行动之一。这对能够在无等待状态下处理某些"Tile cycles"的设计非常关键,其中一些将在第2.2.6节中描述。在许多设计中,如果命中发生,则更新缓存,如果未命中,则忽略写周期。在其他设计中,行被自动失效(写失效)。这种方法用于克服硬件的某些速度限制。第三个选择是,无论写周期是命中还是未命中,都要写入缓存行。最后一个行动被称为写更新,并且通常用于在直接映射设计中写入行之前无需检查缓存命中。缓存控制器可以在处理器指示开始写周期时立即开始写周期。图1.15的缓存示例使用写更新策略。当然,在多路缓存中,在未命中周期上更新缓存行将不起作用,因为在检测到缓存命中或未命中之前,控制器不会知道哪个路应该更新。想象一下,在两个不同路的缓存中有相同地址的更新和旧副本之间的问题!
在未命中时更新缓存与不在未命中时更新缓存之间的吞吐量差异似乎尚未得到深入探讨。直觉上,大多数程序会在写入数据之前读取它,除了将数据推送到栈上以及内存指针初始化的情况,其中会将立即值写入主存储器位置,然后多次重写。如果缓存设计人员可以选择这两种方法中的任何一种,那么最好与程序员讨论。无论使用哪种方法,写直通缓存始终在所有写周期期间更新主内存。
复制回写缓存不总是更新主内存,但通过将数据仅写入缓存,大大加快了写周期的速度,而不是将数据写入主内存。这具有三个主要优点。首先,写周期比每次CPU写操作都需要主内存周期时要快得多;其次,某些写周期(如循环计数器和堆栈条目)仅会被写入主内存的一小部分次数,远少于CPU尝试写入它们的次数。第三,在紧密耦合的多处理系统中,处理器在主内存总线上的时间比例较低,这是一个问题(我们将在第4.2节中深入处理)。
然而,这些优点是有代价的。在复制回写缓存中进行清理需要考虑很多问题,特别是在多处理系统中。最基本的问题是如何处理已写入缓存但未写入主内存的数据。在某些时候,主内存需要更新缓存中更新的数据。通常,当要从缓存中移除更新行时会出现机会。显然,如果该数据只是像在写直通缓存中那样被覆盖,新数据将被破坏,整个程序的数据完整性将受到影响。因此,必须实现一种方法,使得更新的行在从缓存中移除时能够传输到主内存。当数据在被替换时被写回主内存的过程被称为驱逐或释放(被驱逐的行称为受害者)。一些不常用的驱逐周期术语包括复制回写、写回、写出和受害者写入。我将使用"驱逐"这个词,以便不会误解我正在讨论缓存的写策略还是正在处理的周期。

一个非常简单的实现复制回写缓存的方法是将每个要被替换的有效行都写回主内存,无论它是否实际上被处理器写入。这将导致缓存浪费大量总线带宽进行不必要的主内存写入周期,因为所有未被CPU写入的行都会被驱逐。使用此方法的另一个问题是,所有行替换将需要两倍于写直通缓存的时间,因为写直通行替换只需要一个主内存读周期。为避免这种负担,缓存通常实现一种方法来表示缓存中的一行是否比它所代表的主内存位置更新。最简单的方法是使用另一个位来表示缓存中的每一行,这个位称为脏位。在缓存控制器设置该位时,已在缓存中写入但未在主内存中更新的数据会被标记为脏数据。像有效位一样,通常每行缓存都有一个脏位(如图2.14)。在缓存未命中周期中,将检查要被替换的行,如果其脏位被设置,则当前缓存行的内容将被驱逐回主内存。
脏位并不是表示缓存行脏状态的唯一方法。其他更复杂的方法正在广泛使用,并且将在第4章中进行探讨。一般来说,这些其他方法利用了可以通过通常用于有效位和脏位的两个位编码超过三个状态的事实。非有效状态可以存在脏位设置或清除,只有这些状态中的一个对协议来说是真正必要的。剩余的状态可以用来表示另一个缓存行状态。
值得一提的是,一些分离式缓存设计使用复制回写数据缓存,但甚至没有适应指令缓存中的写周期的逻辑,因为这样的周期根本不会发生。
2.2.5 Line Size
在2.1.3节中,我们推迟了对缓存行的真正定义到本节。缓存行是具有唯一地址标记的缓存最小部分。有些研究人员将此单元称为块(block),而其他人则称其为条目(entry)。使用术语"块"的人在2.1.3节中定义了线填充(line fill)。
到目前为止,我们所示范的缓存中,所有的行都只有一个字长。另一种描述它们的方式是每个缓存中的字都有自己的地址标记。

使用超过一个字长度的行大小有两个原因。首先,如果行的长度为两个或四个字,那么缓存标记RAM只需是缓存数据RAM深度的一半或四分之一。尽管这在离散缓存中并不是一个很大的节省开支,因为不同密度的静态RAM在价格上的差异并不太大,但读者可以欣赏到在集成缓存设计中选择更长行的尺寸节省芯片面积所带来的好处,其中被静态RAM占用的芯片面积与RAM阵列的大小成正比,而芯片成本则与芯片大小成正比(见图2.15)。

选择更长的行大小的第二个原因是因为从主存储器进行的多字传输(突发或突发填充传输)可以设计得比填充相同数量缓存字所需的独立传输次数更快。如果整个缓存系统都是围绕从主存到缓存的多行传输进行设计的,该设计可以更充分地利用可用的总线带宽。这一点很容易理解,并且在图2.16中有所说明。使用独立传输时,处理器/缓存子系统必须在每个事务开始时输出一个地址。经过总线延迟时间后,该地址的数据被放置在总线上,然后处理器随后可以更改地址以请求下一个字。这在图2.16a中显示。图2.16b显示了一个突发传输周期。首个突发传输的字的地址被输出,并且与之前相同的延迟时间传输该字的数据,但是主存储器意识到正在发生突发传输,然后在单个CPU时钟周期内以互相间隔一个周期的方式输出第二、第三和第四(或更多)个字,以允许该行以最大可能速率进行重新填充。对于一个四周期的填充,这被称为2:1:1:1填充,因为第一个数据在第二个周期后返回,并且每个后续周期都以无等待状态向缓存提供数据(如果每个周期都有等待状态,那么填充将被称为3:2:2:2)。
这种方法显然利用了局部性原理,并且很容易说明在某个点之后,它停止帮助系统并成为负担。关于它何时成为负担的问题存在一些争议。我将从两种极端情况来论述我的观点。首先,看看具有单字行的高速缓存。如前所述,更新缓存中任何单个字需要一个输出地址和一个输入数据。之后,CPU可以开始再次运行,可能不会发生另一个高速缓存未命中。然而,每个更新缓存中的单词都会产生一个延迟周期。另一个极端是,我们看一个例子,整个高速缓存只有一行,无论高速缓存的大小是512K字节还是更多!一个地址代表整个高速缓存,要么整个高速缓存是命中的,要么整个高速缓存需要被替换。有几种机制会使具有这种高速缓存的系统的性能比任何未缓存的系统都差。在读取未命中周期中,CPU会被阻塞,因为整个行正在被替换。对于一个四字线的例子,512K字节高速缓存需要完成131,072 (128K)个总线周期,这可能比处理器执行的大多数循环更长。另一个问题在于,如果你看一下典型的代码片段,比如图104中所示的代码片段,大多数程序同时在几个空间中执行。这就是关联性起作用的原因。如果整个高速缓存只有一行,任何堆栈访问都需要进行一次行填充,然后堆栈指向的代码需要进行一次填充,然后被该代码访问的数据空间需要进行一次行填充,以此类推。尽管具有每行一个字的高速缓存会发生一定程度的抖动,但直观上可以认为,任何减少标签RAM中包含的不同地址数量的措施都会产生更多的抖动。就像不同的空间似乎偶尔会互相干扰一样,它们被赋予更大的鞋子以便更多地互相干扰。不幸的是,没有全局最佳的行大小。根据某些测量,两个字的行大小是最佳的。其他研究人员坚信八字的行大小是最佳的。一些研究人员指出,尽管系统的性能停止改善,但未命中率仍然可以随着行长度的增加而继续降低。

一种非常好的方法是,一些设计者使用比CPU到高速缓存接口更宽的字来增加行替代的速度。例如,假设处理器使用32位字,高速缓存的行大小为四个字。高速缓存可以被设计成实际上是128位宽(4 X 32),但通过某种多路复用器只向CPU提供32位(图2.17)。每当发生高速缓存未命中时,高速缓存将在单个延迟中与128位主存进行128位交互。这很快解决了CPU等待突发周期完成的问题,但并不能解决增加的抖动问题。大多数设计者认为抖动是两种问题中较小的问题。三菱半导体采用了与此类似的方法,他们称之为"Cache DRAM"。该芯片是一个带有额外4K X 4 SRAM元件的4兆位DRAM。在高速缓存未命中时,64位高速缓存行在一个时钟周期内从DRAM移动到SRAM,反之亦然。由于外部数据路径是4位,因此具有32位总线的系统将使用八个设备,使得行替代在一个时钟周期内总共是十六个32位字!这主要是因为SRAM或DRAM内部的阵列通常是方形的,并且其内部单词非常宽,它会在外部世界上变窄(就像图2.17中所示的多路复用器),而且在单个芯片内,宽字的惩罚并不像使用行业标准离散器件实现的板上空间和芯片计数的惩罚那样严重。
在撰写本文时,微处理器内部高速缓存行突发填充序列存在两个不同的派系。第一个是IBM和Motorola在Power PC微处理器的内部高速缓存上使用的方法,其中较低的两个字地址位用于指定在四个字行中替换的字。这些位从缓存未命中的确切地址开始逐个增加。另一个派系包括Intel PC处理器。这些高速缓存会根据未命中地址是奇数还是偶数而进行递增或递减。两个计数器都会在未命中地址的两个较低位数值溢出后循环,并且不会对更高位数值进行增量。乍一看,Motorola算法似乎更实用,因为代码按顺序执行。但这种观点忽略了栈(也存储在缓存中)在读取时是倒序计数的事实,以及某些其他数据结构也是如此。另一个不那么明显的点是,由于计数器在溢出后会进行循环,无论使用哪种序列,任何高速缓存未命中都会用相同的四个字填充高速缓存行。尽管我确信将来会有人进行研究,显示出两种序列之间的微小差异,但我真的不认为行填充序列对高速缓存的性能有多大影响。
在设计使用ECL CAM的旧系统中,写策略也是行大小决策的一个因素。如果选择了写更新而不是写使无效策略(即写未命中会覆盖高速缓存中的现有行),并且写入的长度小于行长度,那么高速缓存如何表示只有部分行是有效的?有几种处理此问题的方法,我们将在这里探讨其中两种。


第一种方法是写分配。当发生写未命中时,正在写入的缓存行的余下部分会从主存中获取,并且写入数据会与该行合并,然后将该行写入缓存。更详细地说,一旦检测到写未命中,缓存控制器就会开始进行行替换周期,可能会从缓存中驱逐一行脏数据,从主存中读取将要复制到替换行的数据。被写入的地址处的数据要么不从主存中传输,要么立即被CPU的输出数据覆盖。在周期结束时,缓存行被填充了来自主存的数据,并更新了写入的字的值。一些设计者也将这个功能称为合并。图2.18以图形方式显示了写分配。在这个例子中,当CPU尝试将一个字节写入未被缓存表示的地址时,遇到了写未命中。该行从主存中更新,然后在缓存中覆盖相应的字节。写数据是否发送到主存取决于缓存的写策略,但无论如何,任何缓存行的第一次写未命中都会产生很大的惩罚,因为处理器必须在继续之前获取整个行。一些设计者解决这个问题的方法是将写后写入缓存,但这可能会导致缓存硬件过于复杂。

第二种方法涉及分区。将行称为块的人将分区称为子块。在前面的例子中,我们假设所有缓存与主存之间的事务都涉及整个行。当CPU在尝试写入行的一部分时遇到未命中时,通过写分配将该行替换为匹配的行。该行使用一个有效位表示其真实性。分区的缓存设计允许最小的可写数据单元(通常是一个字)拥有自己的有效位,以便每个缓存行包含多个有效位,表示每个分区或子块。图2.19展示了这一点。读者会注意到这个图与图2.5之间存在很强的相似性,在那里,行只有一个字长。缓存数据和每个字的有效位被保留,但是缓存标签RAM被节省下来只有四分之一的条目数。这种方法通常用于在集成的缓存控制器上节省硅片的空间。

当发生缓存读未命中时,目标行将被置为无效,并且请求的字将被带入并添加到缓存中。只为该字设置有效位。通过空间局部性,我们会得出结论,CPU很快会请求附近的某个字。当确实请求了这个字时,如果它适合于同一行内,缓存控制器将更新缓存,并为该行内的字设置有效位,这样现在会有两个有效位被设置。只有CPU实际请求的字才会从主存中带入;但是,每个字都需要一个完整的总线周期,而不是通过连续传输提供的简略周期。在我们的写未命中示例中,将清除要替换的行的所有有效位,并且仅为写入缓存的字设置有效位(图2.20)。这使得缓存写未命中更新可以与不更新缓存的写周期以相同的速度发生。这比在需要多字读取周期的现场分配系统中等待要快。如果使用写缓冲区,或者如果将此方法与回写策略一起使用,写周期可以与CPU操作的速度一样快。在使用分区的系统中的一个额外好处是,现在可以基于逐字处理主存到缓存的接口,这对于将缓存添加到旧的总线体系结构非常重要,该体系结构不支持连续读取周期。
回顾过去几段,我们可以看到,尝试缓存写未命中周期会引起很多麻烦。当然,处理整个问题的最简单方法是首先禁止在写未命中时进行行替换。写透缓存不太可能从缓存写未命中中获得太多好处,因为通常先读取数据,然后写入相同的位置,除非是对给定地址的第一个堆栈推送。虽然在写未命中时不允许进行行替换的策略可能会导致在回写缓存中增加总线流量,但即使在这种情况下,惩罚也会很小。
2.2.6 Write Buffers and Line Buffers
一个简单的写透缓存设计可以与微处理器配合使用,以减少有效主存读周期时间;然而,它对主存写周期时间没有影响。通过添加一个写缓冲区或更简单地说,一个缓冲写的方式,可以改善有效主存写周期时间。在没有写缓冲区的写透缓存设计中,每次执行写周期时,微处理器必须完成一次主存总线事务。这将导致它遭受相关的系统总线延迟。然而,在使用写缓冲区的系统中,微处理器将数据写入缓存,并在写周期内将数据、地址和相关状态信号写入写缓冲区(但不写入系统总线)。然后,微处理器继续访问缓存,而缓存控制器同时将写缓冲区的内容下载到主存。这将有效减少写入主存的周期时间,从需要进行主存周期的时间减少到高速缓存的周期时间。使用写缓冲区几乎可以消除写透和写回缓存之间的性能差异。就像有关最具成本效益的缓存大小和关联性的研究一样,类似的研究也聚焦于写缓冲区的适当深度。出于经济考虑,许多设计使用单层深度。一些半导体公司生产了四层写缓冲区,并声称这种配置将在99.5%的时间内允许零等待的写周期,但显然适当的写缓冲区深度,就像大多数其他缓存设计权衡一样,很大程度上取决于主存访问时间和正在运行的程序的写周期活动等现象。写缓冲区引发了它们自己的问题。让我们来看一个先后进行栈推送和弹出的情况,其中推送是一个未命中写入缓存的操作。自然地,弹出操作也将遭受一次读未命中周期。即使写缓冲区只有一个层级,推送的数据在弹出执行之前可能还没有传输到主存中。除非小心处理,否则弹出操作将在主存更新推送数据之前读取主存。解决这个问题的一个简单方法是,在将写缓冲区的内容加载到主存之前,禁止缓存执行行更新。从写缓冲区强制写入主存被称为清空写缓冲区。另一种方法是始终在写未命中时更新丢失的行。
当使用多级写缓存时,问题变得更加棘手。要么在缓存可以继续进行行更新之前必须完全耗尽写缓存,要么写缓存必须满足数据请求。某些商用写缓存允许最后一种方法,并且对于它们包含的任何未写入主存储器的数据都表现得像是完全相联的高速缓存。这有时被称为受害者高速缓存,被宣传为用于逐个或逐两个线减少扰动的一条或两条完全相联的高速缓存。无论数据在队列中的位置如何,如果读取周期请求该数据,写缓存将向CPU提供数据而不是向高速缓存或主存储器。这种方法的一个版本也被称为污染控制高速缓存。

在某些多级写缓存中提供了一些不错的功能,比如字节收集。在执行文本操作的程序中,以及在一些早期版本的程序中,可能会写两次或四次到同一个字地址,以更新该地址内的单个字节或字节对。举个例子(图2.21),假设一个四字母单词正在逐个字节地写入到地址09AF 45ED。在这个例子中,处理器连续输出相同的地址四次,并每次输出一个分离的字节写入命令。如果我们的写缓存有四级,则在此操作期间所有四个缓存都会很快被填满。字节收集写缓存注意到地址之间的相似之处,并继续更新尚未写入主存储器的单词内的字节。执行此操作的硬件与用于使用待处理写缓存数据满足读取请求的硬件相同,因为两者都是由地址匹配启用的。在更严重的情况下,一个字符串可能会被写入到两个位置,相同的数据会交替地写入到两个地址。字节0先写入0000 0000,然后写入FFFF FFFF;然后字节1写入0000 0000,然后895F FFFF,等等。在字节收集写缓存中,无论数据呈现的顺序如何,两个缓存位置都将收集这两个位置的数据。
在使用多字高速缓存行的高速缓存中,称为写合并的类似机制将单独的字写入组合成单个高速缓存行写入。所有这些的一个结果是,主存储器总线上的流量看起来与CPU引脚上的流量完全不同。如果写缓存数据不能满足缓存未命中的请求,则在先前的写周期被从写缓存中下载之前,可以在主存储器总线上放置一个读取请求。像刚才所示的交替字节写入一样,一系列连续的八个单字节写入紧随其后将变为两个简单的四字节单词写入。换句话说,八个单字节写入,然后是读取周期,在主存储器总线上可能会出现一个读取周期,然后是两个四字节单词写入。这极大地扰乱了系统总线上事件发生的顺序(好像由于缓存吸收了程序的大部分局部性,主存储器读取周期的随机性还不够糟糕)。写序、读序、顺序一致性或一致性程度用来表示写和读周期的顺序不同。当写周期和读周期在系统总线上接近CPU所遵循的序列时,写序被称为强。最强的排序被称为处理器排序,表示周期在总线上的顺序与处理器上完全相同。如果序列完全混乱,则写序称为弱。不过,让我们简单地观察一下,写序可能在I/O或多处理事务中变得重要,其中一个位置被读取,并根据其值将更正因子写入到不同的地址。在随后的读取中,为了确定先前写入的效果,可能会在前面的写入通过写缓存之前放置在系统总线上。当然,几乎没有系统会出现这种情况,但在实时系统中,这可能会导致一些难以找到的不稳定性。
写缓冲区不仅仅在CPU与主内存接口处使用。某些处理器的写周期定时非常严格,其缓存无法在零等待状态下接受写周期。一些设计者通过在CPU和缓存之间放置单级写缓冲区来解决这个问题。在其他设计中,通过向主内存本身添加写缓冲区可以加速主内存的表现速度。尽管写周期仍然受系统总线延迟的影响而延迟,但主内存的写周期时间对处理器来说是隐藏的。



写缓冲区还可以在复制回写架构中有益地使用,这被称为并发线回写、串行线回写或后台线回写。并发线回写是一种将驱逐周期从处理器中隐藏的方法。要解释清楚这个方法比较困难,因此会使用图2.22中的图表进行帮助。在典型的读失效驱逐周期中,在读取新替换线开始之前,驱逐的线被复制回主内存。这导致有效主内存访问时间翻倍,这是一个相当糟糕的交易。在并发线回写中,读取周期是首先发生的。这可以通过两种方式之一来实现。在第一种方式中,称为缓冲行传送,替换线被读入到行缓冲区(类似于输入写缓冲区,将主内存数据写入缓存),并用于满足CPU的即时需求。稍后,当CPU在执行其他任务时,驱逐的线被写入主内存,而行缓冲区被写入缓存。这涉及到一些非常复杂的时序处理,特别是因为缓存几乎从不被CPU单独使用。
执行并发线回写的第二种方法更简单,但如果主内存非常快且缓存行足够长,则会减慢线替换的速度。使用这种方法时,一旦检测到读失效,就会启动主内存读取周期。在缓存控制器等待主内存响应的同时,被驱逐的线正在加载到输出写缓冲区中。希望写缓冲区填充所需时间比主内存的访问时间少,以便主内存数据不会无法获取,等待缓存控制器完成将驱逐线移入写缓冲区的操作。一旦主内存数据和驱逐线完全复制到写缓冲区中,就将主内存数据呈现给CPU,并作为行更新复制到缓存中。一旦行更新完成,CPU可以继续从缓存中操作,而写缓冲区将其内容作为后台任务复制到主内存。
尽管这两种方法都非常复杂,但将有效主内存访问时间减半的优势是值得付出努力的。因此,并发或总线并发的意思是允许同时发生两件事情的方式,这是缓存设计中追求的一个特性。就像写顺序一样,并发被称为强并发,如果有很多事件可以重叠,则并发性强;如果只有少数事件重叠,则并发性弱。如果你稍微思考一下并发,就会明白一种增加并发的方法,即写缓冲区,会破坏写顺序或缓存的一致性。换句话说,具有弱并发性的系统将表现出强一致性,而具有强并发性的系统将表现出弱一致性。天啊!
行缓冲区不仅在掌握并发性方面有帮助,还倾向于加快支持多字线的任何类型的缓存中的线替换速度。当设计者决定线补充策略时,有两种选择。一种不需要行缓冲区的选择是将处理器保持在等待状态,直到整个线被补充完毕。在完成完整的补充之后,处理器被允许继续执行。这是合理的,因为进入处理器内部缓存的数据路径与缓存线补充绑定在一起,并且无法轻松地满足CPU从外部输入数据的需求。这样的系统通常使用线填充顺序或线序列,将最后请求的数据(也称为所需字最后和关键字最后)作为最后一次突发写入缓存的数据。线的填充始于与缓存失效要求所需不同的地址,并以错过的字结束。
基于线缓冲区的线填充策略可以称为流式缓存。缺失字同时被馈送到线缓冲区和CPU中,然后CPU被允许继续执行,可能请求线中的下一个字,这可能是在那一刻正在更新线缓冲区中的下一个字,或者甚至从完全不同的缓存行读取,而剩余的缺失行正在读入到线缓冲区中。其他基于分段缓存的流式缓存设计不使用线缓冲区,允许同时执行和缓存行更新,但会以缓存行更新可能被CPU与缓存之间的交互在更新线地址附近中断的代价为代价,导致缓存仅更新其行的一部分。这被称为中止行填充,因为CPU有能力停止行填充,以便在不同地址处服务缺失。当然,在非分段缓存设计中也可以这样做,如果设计者不介意在中止行填充的缺失上使整个线无效。另一方面,如果允许行填充继续,并且导致CPU等待第二次缺失被处理,那么行填充被称为非阻塞的(CPU不能阻止正在进行的行填充)。大多数具有线缓存的缓存使用非阻塞策略。为什么一种策略比另一种更好,这似乎一点也不直观。流式缓存有多种名称(就像本书中的其他所有东西一样),例如旁路、装载转发、早期继续或早期重启设计。现代处理器中都存在流式缓存的示例。
通常,如果使用线缓冲区加速缓存行替换,则使用循环获取(缺失可能发生在线的中间位置,在这种情况下,突发会环绕,直到获取整个线)。循环获取的线填充顺序称为期望字先、先请求的数据或关键字先。
某些缓存会提前取出预期被缓存错失的下一行。通常,上次获取的最后一行后面的行被预取并存储在线缓冲区中,假设所有缺失都是必需的。即,获取的行以前不曾驻留在缓存中。预取下一次缺失的缓存称为始终获取或Class 2缓存。相反,仅获取错失行的缓存称为Class 1、按需获取或按故障获取缓存。
在阅读完本节之后,回顾一下加粗显示的新术语的数量。然后决定是要成为缓存设计师还是藏族僧侣。
2.2.7 Noncacheable Spaces
主存中的某些空间不应该被缓存。最明显的例子是用作设备输入的主存地址范围的部分。这对于那些不区分内存和I/O地址的处理器特别重要,例如Motorola的680x0系列。例如,如果处理器在等待开关面板状态变化时循环运行,则如果从开关面板读取的第一个数据已经被缓存,那么任何状态变化都不会被 notice到。一些处理器还使用主存地址范围的一部分与协处理器进行通信。同样,不能假定数据是静态的,所以缓存的副本可能会过时。最后,多处理器系统通常通过在专用主存位置设置和清除标志进行通信。如果一个处理器设置的主存标志不能被另一个处理器读取,因为后者正在读取缓存的副本,则无法进行通信。
所有这些示例都是容纳未缓存地址或不可缓存地址(NCAs)在高速缓存中的原因。实现这个方法非常简单。地址解码器向高速缓存发出信号,表示当前的内存请求位于不可缓存空间内。高速缓存控制器反过来禁止所选行在缓存中更新。在某种程度上,高速缓存控制器将这个周期视为既不是命中也不是未命中,但会让高速缓存在这个特定的周期中表现得好像不存在一样。
具体实现取决于系统设计者更改系统体系结构的自由程度。在某些系统中,所有高或低内存都可以被声明为不可缓存,缓存将忽略最高位设置或清除的地址。在其他系统中,每个加入的I/O板将通过背板上的专用信号向外部传递其自身地址空间中的哪些部分是不可缓存的。最困难的情况是将高速缓存添加到以前没有高速缓存的体系结构中。在这种系统中,通常的方法是将地址解码器实现为缓存的一部分,紧挨着CPU,并将所有可能的不可缓存地址传送到高速缓存控制器,即使针对可能未安装在系统中的设备。如果解码器禁止缓存设备不存在的多个空间,则自然会影响缓存性能。
2.2.8 Read-only Spaces
与不可缓存空间类似的是只读空间。这是一个可缓存的地址,缓存行仅在缓存未命中读取时更新,而不是在写入命中时更新。再读一遍上面那句话,因为它似乎没什么意义。为什么缓存要包含从主存地址读取的最后数据,并且在写入相同的主存地址时不更新该数据呢?
这个奇特的概念的存在是因为某些I/O板设计师在其I/O设备输出寄存器的主存写地址上覆盖了一个包含I/O设备驱动程序的PROM。实际上,同一地址处存在两个内存空间:一个是只读空间,一个是只写空间。只写空间是I/O设备的输出寄存器,只读空间是I/O驱动程序的PROM。显然,PROM很适合被缓存,但由于当CPU写入I/O设备的寄存器时,PROM的内容不会被修改,因此在写入周期发生时,PROM的缓存版本不能被更改。
这个问题有时被称为写入副作用,在PC中往往会出现,因为其主存空间有限,所以设计师使用PROM-I/O重叠来节省空间。
处理这个问题的最简单方法是将在某个范围内的所有地址标记为只读或写保护位,并在缓存中进行更新时,将解码器的输出直接发送到一个独立的位,类似于另一个有效位。这个位在只读地址处的任何写命中周期中自动禁止更改缓存数据RAM的内容。
这种方法在某些二级缓存控制器中使用。写保护位用于允许在二级缓存中缓存地址,但不允许覆盖。二级缓存控制器进一步利用该位来禁止处理器的内部一级缓存甚至包含该位置的副本。这样可以防止在CPU写入周期期间不可避免地更新一级缓存的副本,因为CPU的内部一级缓存通常没有只读位(参见第2.2.10节)。
2.2.9 Other Status Bits
前面几节中描述的所有处理细节位可以称为状态位。这类别包括有效位、脏位、只读位、LRU位以及与每个缓存行一起存储的其他任何不是数据或地址标签的位。
如果你做一些数学计算,你会发现通过明智地使用状态位可以减少缓存的总位数。一个很好的例子是使用分段行或更长的行来摆脱标签条目。虽然这些方法在离散缓存设计中可能帮助不大,但片上缓存确实可以充分利用任何减少晶体管数量的机会。这就使得在承诺特定缓存设计之前,测量所有设计选项的实际性能变得尤为重要。另一种节省内存位的方法是忽略更高位的标签位,如果这些位表示系统未使用的地址位。
添加状态位到设计中还有其他原因。控制域标识位可用于标识缓存条目的用户级别。有时,这些位被用来确定在多任务操作系统中进行任务切换时应该使哪些缓存条目无效。在某些缓存设计中,任务号被存储以解决在逻辑缓存设计中可能出现的别名问题。图1.15中的逻辑缓存使用了这种方法,其中包含处理器的功能代码位FCO-FC2。
锁定位用于防止条目从缓存中移除。这可以在实时操作系统等对时间要求苛刻的软件应用中加快中断响应速度。锁定位在多路缓存中最有意义,但也有可能出现在直接映射设计中。读者/写者锁是一个位,它可以阻止未经授权的处理器或进程覆盖缓存行的内容。
毫无疑问,你会遇到其他以某种独特方式使用状态位的缓存。如果你将它们视为缓存的个别行的标记,那么理解它们会更容易。在第4章中,我们将看到一些设计,其中缓存行的状态被编码以减少用于存储行状态的状态位的数量。
现在让我们继续探讨一些与每个缓存行中的状态位无关的方法。
2.2.10 Primary, Secondary, and Tertiary Caches
如果你已经看到这本书的这一章,那么你应该知道缓存在命中时很好用,但真正无法加速缺失的周期。那么问题就成为了成本/性能的权衡,优化缓存大小和速度并把它们与构建这样的系统的实际情况相比较。微处理器设计者面临着芯片面积与缓存大小之间的权衡,因为一个芯片上的缓存自然可以比任何涉及信号被路由到和离开芯片本身的东西运行得更快。

我们假设处理器使用小而非常好的缓存,并使用慢速DRAM来实现主存储器以保持存储器阵列的成本。那么如何使这样的系统运行得更快呢?设计师会试图在芯片内缓存未命中周期期间最小化主存储器的访问时间,虽然诸如交织主存等技巧有所帮助,但仅在一定程度上有效。解决这个问题的一个非常有效的方法是在芯片外部构建一个更大但速度较慢的缓存,并在芯片内缓存未命中周期期间使用此缓存来加速主存储器的表面访问时间。在一个使用两个级联缓存的系统中(见图2.23),与处理器更密切相关的缓存称为第一级或主要缓存,放置在CPU/主要缓存子系统和主存储器之间的缓存称为第二级或次级缓存。当然,可以添加更多级别,并将其称为第三级或三级缓存等,用于在第二级缓存和主存储器之间的缓存。有些架构师使用Level 1或LI表示主要缓存,Level 2或L2表示次级缓存,Level 3或L3表示三级缓存等等。无论使用哪种术语,比正在讨论的缓存更接近处理器的缓存称为上游或前身缓存,比靠近主存储器的缓存更接近的缓存称为下游或后继缓存。
可以放心,世界还没有变得那么复杂。虽然作者看到过一些具有次级缓存的系统,但在撰写本文时使用超过两个缓存级别的系统极为罕见,尽管一些多处理器系统使用共享的第三级缓存,用于具有自己的主要和次级缓存的处理器。使用多级缓存方案的原因不仅限于上述原因,例如在CPU芯片上节省die面积。某些处理器结构具有内置的缓存控制器,可以自动限制主要缓存的大小和策略。另一个原因是所需缓存的大小或类型可能无法使用最先进的静态RAM来实现。大型RAM tend往往比小型RAM慢,因此设计者可能会陷入在小型快速缓存和大型较慢缓存之间妥协的困境中。问题可以分解,使得上游和下游缓存在系统中一起使用,从而减少必须做出的妥协程度。这种方法已经在单个芯片内讨论过,其中处理器芯片包含CPU加上一个小的主要缓存和一个大的次级缓存。等等,你说。芯片上的两个缓存速度不会相同吗?其实不是。RAM越大,实现其地址解码器所需的逻辑层数就越多。遵循RISC论点的人们(在节3.1中,通过将关键路径逻辑延迟元素的最小化来减少CPU的周期时间)将观察到,通过减小缓存大小可以使关键路径变得更快。在这种芯片中,缓存和CPU紧密地设计在一起,所有的努力都用于减少周期时间。我们将在第5.2节中展示一些例子。
最后一个原因,也是用来合理化使用二级缓存的最常见原因,是系统设计师被指定使用一个具有内部缓存的行业标准CPU,而该缓存并非为当前系统设计而设计。一个很好的例子是,围绕着任何一个具有"直通缓存"的现有可用处理器设计的多处理器系统。多处理器系统中的缓存通常设计为尽量减少系统总线流量,远远高于使用直通缓存的可能性,因此这些系统的设计人员经常选择用二级回写缓存补充芯片上的直通缓存。
当然,二级缓存的设计很大程度取决于预期从主缓存所需的流量。可以肯定的是,第二缓存所要求的活动与主要缓存完全不同。二级缓存通常处于空闲状态,而主缓存几乎从不处于空闲状态。读/写周期的平衡可能会在主缓存和次级缓存之间完全反转,命中率可能会显著降低,并且请求序列可能会表现出更随机的行为,同时时间和空间局部性的作用可能会减小。这些都为什么呢?我们将一步一步地来解释。
首先,我们假设处理器通常在每十个读周期中执行一次写周期(这再一次是由机器上运行的代码决定的),并且主缓存是一种直通设计,可以满足所有读周期的90%。这是一个相当典型的场景。由于设计是直通的,主缓存不会拦截任何写周期,因此处理器周期的10%自动传递到二级缓存作为写操作。二级缓存会看到多少读周期呢?如果处理器周期的其余90%是读取,其中90%被主缓存满足,那么其中10%将传递到二级缓存,或者总体CPU周期的10%x 90% = 9%。这意味着二级缓存将看到更多的写操作,如果主缓存的命中率高于90%,则平衡将更加倾向于写操作。这种情况在使用回写主缓存的系统中不会发生;但是,从主缓存到二级缓存的写操作仅在缓存驱逐时发生,因此对于写操作而言,时间上没有任何影响。缓存清除可能在实际写周期之后的非常短或非常长的时间内发生。

下游缓存的命中率远远低于主缓存,并且这是有充分理由的。正如我们在图2.9中看到的,随着缓存大小的增加,缓存的命中率逐渐降低。命中率的提高可以被视为一个差异值AH/AS,其中H是命中率,S是缓存的大小。如果主缓存的命中率为90%,而次级缓存将命中率提升到95%(见图2.24),那么次级缓存只是将不命中率减少了一半,因此命中率为50%。这就引发了对次级缓存价值的质疑。为什么要为这么微小的改进付费?如果我们从底层向上看这个问题,我们会发现单级缓存系统的不命中率为10%,而双级缓存系统的不命中率为5%,这意味着双级系统的总线流量仅为单级系统的一半。这是多处理系统中的一个关键改进,正如第1.8节讨论过的,在具有较长主存储器延迟的系统中也非常重要。(顺便说一句,图2.24是虚构的数据,请不要试图用它来证明任何观点或设计自己的缓存)。
如果下游缓存与上游缓存具有相同的行大小,那么合理推断下游缓存的命中仅来自于由于冲突而导致的上游缓存不命中。这很容易理解。次级缓存只会装载与主缓存请求的相同数据,而且是在主缓存加载时同时进行。如果该数据仍然保留在次级缓存中,但被从主缓存中删除,那么只有当主缓存发生冲突导致其拷贝被覆盖时,并且次级缓存没有出现相同的冲突,这可能是由于较大的行大小或较大的地址空间导致的。因此,下游缓存应该要么比上游缓存大得多,要么使用比其上游邻居更大的行大小。
一些设计师选择确保主缓存的内容不包含在次级缓存中。为什么要在这两个地方都存储重复的数据呢?这是对两个缓存更有效利用的方法,但有时这种额外的效率会妨碍实现其他良好的缓存策略。我们将在4.1.1节中讨论一种称为"包含"的缓存策略。有时,从次级缓存中排除主缓存数据变得非常困难,而另一种选择不仅可以缩短设计周期,还可以减少缓存控制逻辑的复杂性和芯片数量。
次级缓存还可以用于在多处理器系统中实现良好的筛选机制,这个主题将在4.1.3节中详细介绍。简而言之,系统越少干扰CPU/主缓存子系统,该子系统运行得越快。次级缓存可以过滤系统对主缓存的干扰,只允许潜在有意义的交互。
2.3 STATISTICAL PREMISES
图2.9被用来说明增加缓存大小的收益递减和增加给定大小缓存的关联性所带来的命中率改善。尽管该图对缓存系统的潜力给出了很好的印象,但它并不准确。真实的缓存统计数据高度依赖于目标系统的确切硬件和软件特性,了解自己系统的确切特点是无可替代的。不幸的是,大多数设计师没有足够的资源来测试他们可能实施的各种缓存组合,因此像这样的图表可能成为某些设计决策的替代选择。
总体上,这些曲线是对预期缓存系统性能的合理估计。已经有几篇论文提供了实际测量数据,它们通常不像图表中的曲线那样平滑。许多曲线显示出波动,这可能归因于程序员在写作时对最大循环大小的限制,或者限制于使用的数据集大小,而且有些性能曲线实际上会交叉,显示出对于较小的缓存大小,某种策略下的缓存表现比另一种策略更好,但对于较大的缓存来说则更差!
2.3.1 The Value of Choosing Policies Empirically
如果我们要严格要求,那么仅在收集了大量的统计数据之后,才能设计缓存,以确定特定策略和缓存大小相对于其他所有策略和缓存大小的性能权衡。更可能的是,设计师会从这段文字中继续阅读一些可用的论文,然后基于这些论文中在完全不同的系统上测得的统计数据来做决策,而这些系统运行的软件与目标系统将要使用的程序完全不同。
这可能导致糟糕的决策,因为这些论文中的统计数据是在与当前设计完全不同的系统上收集的,其中一些更微妙的权衡可能会使论文中的系统表现更好,但实际上可能会阻碍新缓存的运行。实时多程序系统往往需要比MS-DOS等简单的单任务操作系统更高的关联性级别。主存总线协议对替换算法的选择可能会产生重要影响。
接下来的部分应该与系统程序员讨论,希望他们能通过提供帮助测量缓存真实性能的工具来帮助您。
2.3.1.1 Simple Methods of Measuring Performance
软件建模是一种低成本的方法,可以将可能的缓存策略相互比较。通常情况下,目标处理器在未缓存的系统上运行,模拟器会拦截所有内存访问。模拟器被设计成模拟不同类型的缓存,然后使用该模型运行将在目标系统上运行的程序。软件建模的一个很大劣势是它比未使用模型的未缓存系统运行相同程序的运行速度慢了一个数量级(很可能慢了两个数量级)。这很容易理解,因为目标代码的每个指令必须通过模型软件手动输入到处理器中,并且必须为每个引用存储统计信息。增加的执行时间意味着在大型软件库上尝试几种不同的缓存策略可能会消耗相当多的时间。尽管如此,由于此方法的低成本和易于实施性,它仍然是一种受欢迎的选择。
一种不太受欢迎且成本相对较高的替代方法是硬件建模。典型的硬件缓存模拟器包括设计师希望使用的最完整的缓存,并添加开关以更改或删除一个或多个变量(例如缓存大小、写入策略或关联度)。然后使用不同的功能启用或禁用目标程序,并使用生成的执行时间来决定系统的实现。显然,这样的系统需要花费相当大的精力来调试。为了简化设计师的生活,这样的系统有时会以缩放的速度运行,例如最终系统时钟速度的一半或四分之一。只要所有参数按比例缩放(如CPU时钟、主存储器延迟、总线时钟等),就可以获得有效的测量结果,并做出关于最佳缓存策略的好决策。
另外两种简单的方法涉及使用实例执行的跟踪。有两种类型的跟踪:硬件和软件。硬件跟踪通常是通过在总线上放置一个装置来实现的,该装置将以一种可编译统计信息的方式测量实际地址活动,以揭示最佳缓存策略。一种方法是制作可能的替代缓存标记RAM的硬件模型,并测量其未命中率。缓存标记RAM只需与主存储器一样快,因此制作允许模拟几种不同缓存体系结构的RAM是一项简单的任务。虽然说设计另一种跟踪机器来计算特定地址范围内的主存储器访问次数的简单统计数据是相当容易的(可能仅使用逻辑分析仪即可),但这些统计数据不会显示程序局部性,因此最终并不太有用。
软件跟踪,通常称为代码剖析,实现起来更简单,但分辨率稍低。在大多数缓存研究中使用的跟踪是通过微码实现的,但只能在允许修改微码的系统上运行。最简单的软件跟踪是由实时系统中的非可屏蔽中断驱动的。如果用于跟踪的系统没有此类中断,通常不太难设置一个。在中断例程的开始处,程序计数器被推入堆栈。跟踪例程被层层嵌入正常的中断服务例程中,并通过查看堆栈来统计程序计数器的位置。程序计数器并不是唯一可以通过软件跟踪的地址生成器。所有其他指针也可以进行检查,但这将导致中断服务例程的延迟增加。
与刚才提到的更简单的硬件方法一样,必须注意确保中断不要间隔得太宽,以免失去局部性的问题。另一方面,引发过多中断可能会使系统速度变慢,甚至与软件模型的运行速度相当。
使用跟踪的一个好原因是,如果目标系统运行的场景无法以较低的速度充分模拟,并且无法证明硬件模型的成本合理性。例如,某个计算机网络中的单个节点或广泛使用的多用户系统,这些系统都极大地依赖及时的交互,并需要真实地测量来自不可控外部源的影响。
某些编译器支持通过在编译代码中插入额外指令来进行剖析,当激活剖析选项时。这是一种收集有关程序计数器和代码执行情况的信息的好方法,但通常不能很好地帮助你了解数据空间中发生的情况。如果您的主要目标是优化代码访问,这是一个可行的解决方案。我知道有一个程序员通过在汇编清单上使用尺子(由编译器的输出或反汇编后的目标代码生成)测量循环的长度,并计算每个循环平均执行的次数来对其代码进行剖析。他可以根据这些信息确定程序计数器在任何单个位置停留的时间百分比。
2.3.2 Using Hunches to Determine Policies
当然,现在你已经了解了如何测量自己的缓存统计信息,这并不意味着你一定会去做。也许你没有时间,或者很可能是你的市场部门告诉你客户只关注一些特定的功能(可悲的是,与基准测试相比,硬件规格通常会受到潜在客户更深层次的关注)。这时候你就必须依靠你自己(或别人的)直觉。
对于那些没有时间的人,我希望本书中所采用的直观方法能够有所帮助。例如,从之前的讨论中可以很清楚地看出,设计落后于写入透写缓存的系统应更加关注写周期的性能,而不是读取周期;透写缓存的关联度增加或者容量增加通常与将设计转换为拷贝回写缓存的好处(或者复杂性)相比较,显得微不足道;等待状态非常糟糕,没有任何值得付出等待状态代价的缓存策略。我也希望读者有时间阅读一些更专业的关于缓存性能的研究。尽管本书没有试图对哪些论文的价值进行归类,但有很多相关论文可供选择,而且很容易找到一篇你觉得不错的论文,并获取其参考文献中的所有论文副本。然而,请记住,这些论文是针对与你的系统不同的系统、在不同的CPU上运行不同的软件、通过不同的总线结构进行的研究。在缓存设计中,唯一的绝对真理是描述你的缓存如何与你的软件在你的系统上工作的真理。其他的都不那么重要。确保与你的同行们一起讨论每个决策是明智的一步。在事前通过为自己的假设辩护来解决问题比其他任何方法都更有益。
对于那些被要求按照任意规范设计缓存的人,不要灰心。深入思考一下。也许在你承受的限制下,可以将本书中学到的一些内容与之结合,将一个不太理想的策略转变为接近理想的策略。你可能会发现,在缓存设计中有很多变量,你至今可能没有充分利用其中的一两个选项。其中一个好处是,许多这些选项几乎不需要额外的硬件成本,只需在设计和调试周期中多付出一点努力即可获得。
2.4 SOFTWARE PROBLEMS AND SOLUTIONS
到目前为止,我们假设缓存设计者在解决缓存设计问题时没有得到软件方面的任何帮助。通常情况下确实是如此,但在程序员和设计师能够共同合作解决问题的情况下,有一些简单的规则可以用来大大简化缓存的设计。最常遇到的挑战通常是由于缓存被设计来加速一个已经存在多年的系统的性能。软件已经以某种"方式"编写,而缓存可能需要与现有的具有异常的硬件保持兼容性,这些异常必须得到考虑。然而,在完全新的设计中,缓存设计者有幸与设计团队合作,帮助定义整个系统规格。
2.4.1 Trade-offs in Software and Hardware Interaction
通过一些例子,可以更容易理解硬件/软件缓存设计的权衡。一个很好的例子是缓存有效性的问题,在2.1.3节中有描述。软件解决方案是在引导程序确认所有缓存行已被覆盖为有效数据之后,禁止缓存满足任何CPU总线周期,然后同一个程序设置一个标志,使得缓存可以响应。硬件解决方案是为每一行维护一个有效位,并通过硬件复位机制重置所有有效位,在允许缓存响应之前。显然,硬件方法将更昂贵。这种有效/无效的准则不仅在启动时是个问题,还在直接内存访问(DMA)活动中出现,在这种情况下外部设备修改了主存的内容,但未必修改了缓存内存的相应内容。处理这个问题的硬件方法是在支持DMA写周期的总线信号发生时使整个缓存失效(第4章将详细介绍几种更优雅的方法)。处理该问题的软件方法是在调用上下文切换时使操作系统使整个缓存或适当的缓存部分失效。另一个例子稍微难以解释,主要因为它涉及多处理器,这是第4章的主题。在多处理器环境中,处理器经常通过锁定的读/修改/写指令进行通信。一个处理器向主存邮箱位置写入以告知另一个处理器它正在执行的操作。这个问题类似于2.1.3节中提到的I/O地址的问题。如果要读取邮箱位置的处理器引用缓存中该位置的副本而不是实际的主存,那么写入处理器将更新主存而读取处理器却没有注意到。解决这个问题的软件方法始终是将邮箱位置映射到相同的物理内存地址。然后,缓存设计者很容易禁止该地址范围被缓存。在不受控制邮箱位置的多处理系统中,必须制定一些极其复杂的缓存间通信协议。这些协议非常复杂,我不会在这里详细介绍,而是等待我们到达第4章再讨论。
2.4.2 Maintaining Compatibility with Existing Software
如果你审查几个缓存设计,你会开始注意到一些奇怪的曲折,有时必须加入到现有软件编写的方式中。对于许多缓存设计者来说,最大的头痛可能是软件定时循环。只要处理器以恰当的频率进行时钟操作,该定时循环会以恰好正确的速度运行,甚至可能依赖于某种主存延迟。这种类型的编程基本上消除了硬件可以改进的可能性。处理这种问题的唯一真正干净的方法是确定所讨论程序的定时循环的位置,并禁止这些位置被缓存,同时确保处理器时钟永远不会改善。一个更简单的选择是允许用户使用两种操作模式。在IBM PC世界中,已经成为如此配备的系统的两种操作模式的术语称为正常模式和涡轮模式。
在某些实时系统中,中断延迟必须完全相同,无论何时发生中断。在缓存系统中很难实现这一点。假设中断服务例程被编写为计算中断次数,并根据先前的中断活动响应中断输出为高或低。自然地,中断例程和先前的中断活动记录将存储在主存中。该例程在某些中断周期上可能会崩溃,在其他情况下不会崩溃,这意味着中断延迟将是可变的。加上正在运行中断之间的程序有可能有时覆盖一些中断例程,而在其他时间则保持不变。解决这个问题的两个简单方法是禁止缓存在整个中断响应期间工作或禁止缓存涉及中断服务例程的那些位置。后一种解决方案可能过于激烈,因为中断服务例程至少涉及服务例程的代码空间和相关数据空间,以及堆栈。禁止缓存堆栈将对整个程序的性能产生负面影响!
对于那些选择使用逻辑缓存实现的人来说,必须注意理解程序员使用逻辑到物理地址映射的任何技巧。地址别名可以非常巧妙地用于提高程序间的通信,但它们在逻辑缓存设计中真的很难解释。再次说一遍,一个粗暴但有用的方法是简单地禁止缓存可能在某些时候以这种方式使用的任何页面。
2.5 REAL-WORLD PROBLEMS
与本书的其余部分相比,这是一个非常平凡的部分。在这里,我们不会涵盖巧妙缓存设计的技巧和技术,而是解决更棘手的问题:如何让硬件运行,并可靠地运行,以便可以进行大规模生产。
可以合理地假设缓存设计师将使用可用的最快处理器。毕竟,缓存是一种弥补真正主存访问时间与CPU最大吞吐量所需访问时间之间差异的方法。只有当设计师使用最快的CPU速度时,这种差异才会成为问题。此外,缓存通常比使用更快的CPU更昂贵的增加吞吐量的方式(虽然根据图1.1的显示可能并非如此)。
在可用的最快CPU总线速度下,CPU/缓存子系统不能仅作为一个存在逻辑错误问题的数字系统进行检查,而且必须仔细审查和限定时间,设计电路板以适应高频率,并且设计师必须面对很多熬夜。
大多数高速系统设计者允许自己时间,以使系统在比最大CPU速度低10%的缩小速度下彻底工作,然后逐渐增加时钟频率到全速运行,直到所有定时错误都被发现并消除。尽管这听起来很慢,但很少有不如此谨慎的方法更有利可图。
2.5.1 Parasitic Capacitance and Bus Loading
第一次接触周期小于50ns的设计师往往会对高速系统总线信号所需的关注和注意力感到惊讶。在较低的速度下,电容负载所消耗的几纳秒可能会在详尽的时序分析中提到(如果有进行的话),但可以通过使用稍快、稍贵的部件轻松适应。在20ns的周期时,考虑到处理器的输出延迟和建立时间后,设计师发现即使在降额计算之前,缓存也需要使用最先进的组件速度。不准确的降额计算可能会造成两方面的伤害。过于乐观的降额计算可能导致缓存根本无法工作,而过于保守的降额计算可能会阻止缓存的设计,可能使竞争对手先下一步。高速处理器往往被规定为小负载(50pF),原因有三。首先,处理器制造商不希望由于在过于严格的负载条件下测试其部件而导致产量下降,特别是对于根本没有使用的引脚。这是他们给自己留出余地的一种方式。第二,在高速下驱动大的输出负载时,必须使用高电流输出驱动器。将这些高电流驱动器设计进处理器芯片会产生很多后果。所有集成电路都在严格的功耗预算内进行设计。快速电路需要更多电流,因此将更多的电流分配给输出驱动器,可用于处理器更快部分的电流就越少;因此,处理器必须运行得更慢。对于正在推向最高速度的处理器来说,这不是一个好交易。高电流输出驱动器还会在处理器的内部地线上产生很多噪音,可能会混淆一些内部阈值,导致性能下降甚至位错误和死锁。CPU制造商指定输出到较轻负载的最后一个原因是,测试设备往往是以轻负载的方式提供的,而增加更重的负载对于CPU制造商而言既是负担,又是难题。如果要将额外的负载放在测试仪上,应该看起来像什么?似乎没有两个系统设计师能够达成共识,对于CPU芯片来说,"典型"的输出负载是什么样子的。一种被普遍接受的降额计算方法是,对于超过处理器或其他驱动设备指定输出负载的每20pF负载,为信号添加1ns的传播延迟。这对于轻型驱动器(如CPU和存储器)有效,但某些逻辑输出可以驱动更多或更少的负载,请参考器件的数据手册(如果提供了该数据)。尽管我从未听说过用于印刷电路板线路的电容效应的一致数字,但每英寸1pF是一个数字。这样累积起来非常快,所以必须进行仔细的布局设计。

例如,让我们对一个CPU输出进行降额计算,该输出驱动着四个128Kx8缓存数据RAM和三个128Kx8缓存标签RAM的地址输入,以及沿着一条30英寸追踪线的地址缓冲器(听起来很大,但在双面PCB上运行这样的追踪线是尽可能短的)。地址缓冲器和所有RAM都将具有7pF的输入电容,处理器被规定为50pF。表2.1展示了这个系统中较低地址位的典型降额计算公式。
为什么这仅适用于较低地址位?请记住,这些是使用的地址位,因此每个使用的地址位必须路由到所有标签RAM和所有数据RAM的地址输入。标签位只需传递给一个标签RAM和地址缓冲器(除非使用离散比较器,这会增加负载)。请记住,在访问缓存数据RAM时不使用标签位。此外,在使用多字线或节选的系统中,地址位低于设置位的负载将不同。此外,双向总线必须为总线上的每个驱动器执行这些方程。这听起来好像只涉及数据位,直到你意识到在拷贝回写缓存中,地址是从标签RAM获取的。我们将在第4章中看到,在高速系统中,CPU/缓存子系统的地址输入也可以由地址缓冲器驱动。在高速系统中会有大量这些小的计算!
解决一些降速问题的方法之一是使用多芯片模块(MCMs)。在本书写作时,多芯片模块是一个热门话题,但尚未得到广泛应用。英特尔奔腾Pro是少数可用的MCMs之一。在MCM中,CPU、高速缓存和所有向外界的缓冲器都安装在底板上,最好是没有被封装过的。这种技术的优点是,未封装的设备的输入电容较低,并且模块底板能够使用更短的连接以较低的电容每英寸承载信号。此外,如果所有不需要放置在模块输出引脚上的信号都允许使用更小的输入/输出摆幅,节点电容的充电/放电周期将变小,使得模块的芯片可以以更高的速度运行。 MCM的缺点是制造模块的成本很高,往往需要使用唯一供应的RAM和逻辑。再加上如果模块上的一个芯片失败需要返工,成本就会飙升!可测试性也是一个大问题。在短期内,看来MCM将仍然是垂直整合公司高性能和高成本系统的领域。一个合理的折衷方案是使用由封装部件组成的精心设计的模块,放置在标准电路板上,就像英特尔标准处理器所做的一样。另一个令人烦恼的现实关注点是时钟偏移。时钟偏移可能是由明显的机制引起,如使用不同的缓冲器输出来驱动两个不同设备的时钟输入,通常是由于时钟线上的重负载。最糟糕的情况是如果两个不同包装中的两个不同缓冲器用于驱动相同时钟信号的两个版本。一个设备可能特别快,并且安装在板子的较冷部分,而另一个设备允许变热,即使在较冷的温度下,也以组件制造商发货的设备中的较慢端运行。热会减慢硅的速度。不太明显的是,时钟偏移有时是来自对两个完全相同的缓冲器但有不同负载的两条时钟线的不匹配,或者来自两个时钟路径之间的迹线长度偏差。有时,引起问题的偏移来自同一信号线的两个不同分支,这些分支相互之间距离太远。防止时钟偏移成为问题的方法通常是使用来自同一封装中的大信号驱动的小负载时钟线。有些设计师只是对所有时钟输出使用相同的八进制缓冲器(这些设备的引脚之间的偏移量没有经过测试或保证),但现在有特别为此功能设计和测试的设备可用,其电流驱动比八进制缓冲器强得多,并经过测试和保证具有最大引脚之间的偏移量。某些时钟驱动器电路甚至使用锁相环将时钟驱动器的一个输出引脚与参考输入同步。阻性负载也可以帮助系统避免由于延迟线效应而产生的问题。任何导线,除非被适当终止,否则将将功率反射回来。这是基本的传输线理论。如果PC板追踪的两端都没有正确终止,则波形可能会产生严重的振荡,甚至可能在输入阈值上反复穿越,导致向RAM中错误地写入周期或读取数据线上的相反逻辑状态的数值。关于这个问题,有许多不同的想法,所有这些都将留给更合格的资源来解释。PC板布局技术既不是作者的长处,也不是本书的主题。但是,不要忽视这个重要问题。面向美国市场的设计师必须担心符合联邦通信委员会(FCC)无线电发射规定的要求。这与刚刚讨论的时钟偏移和终止问题联系在一起,并且是高速缓存设计师面临的挑战之一。
2.5.2 Critical Timing Paths
图2.10展示了直接映射缓存中高速缓存标记RAM的关键时序路径。对于大多数系统而言,这是真正的热点,也是最难解决的时序问题。集成缓存标记RAM和缓存控制器的使用通常取决于所需的缓存标记RAM速度。在某些架构中,缓存控制器是一个单片集成电路,它接受地址输入并向CPU输出就绪信号。虽然这是一种快速的方法,但通常需要使用ASICs实现,因此缓存标记RAM永远不如静态RAM设计的最新技术那样大或快。其他的实现方式包括将所有缓存控制逻辑(除了缓存标记RAM之外)放置在CPU芯片内部,以减少芯片转换,或将标记比较器包含到缓存控制器芯片或缓存标记RAM芯片中。只有最慢的设计才能允许使用离散的缓存标记RAM,后跟离散的比较器,后跟缓存控制逻辑。
考虑设计具有足够响应时间的高速缓存标记RAM和下游逻辑的问题时,一个观察结果是,对于直接映射缓存而言,这个问题可能很困难,但对于多路缓存而言,它变得非常棘手。
回到2.2.2节,我们看到多路缓存中的标记用于控制数据RAM的输出,而在直接映射系统中,数据RAM在启用状态下开始循环。至少有一种缓存控制器使用一种叫做最近使用(MRU)位的方法来实现两路结构,以指示读取线路最近访问的路径。MRU位只是2.2.2节中讨论的LRU位的一个反转版本。使用此方法,缓存数据RAM以与直接映射缓存类似的方式启动循环,并使得缓存标记RAM可以在这种两路设计中像直接映射架构那样慢。仅在路径错误时出现路径未命中,路径最初被启用的路径不正确,CPU被延迟直到选择正确的路径。可以轻松得出,在这样的缓存中可能出现的最高命中率大约为50%,并且在此特定设计的操作中,该路径未命中仅费用一个周期。英特尔的设计师似乎认为路径未命中率实际上低于50%。两路结构与直接映射版本相比的实际性能收益需要压倒这些附加等待状态对整体缓存带宽的不利影响。MRU基于的两路高速缓存是否优于等效大小的直接映射缓存并不直观,只有测量统计数据才能证明或否定这个论点。
在数据环路中可能发生的其他令人烦恼的时序困难包括CPU输出地址通过缓存数据RAM流入处理器数据输入引脚的路径,如果处理器没有内部缓存,它会倾向于每次请求内存数据都会消耗一些CPU时钟周期,而缓存数据RAM的时序可能很紧,但并非不可能。具有内部缓存的处理器倾向于使用多字线,并一次性获取整个线,使用2.2.5节中描述的突发周期。自然地,允许突发填充以尽可能快地运行会带来速度优势,因此设计者将尝试在设计中实现零等待数据RAM。第一个周期很容易匹配。在需要数据之前,地址在一个完整的CPU周期时间内输出。但是,地址保持有效,直到获取第一个字之后,偏移了其它周期的时序,以至于余下周期的访问时间为CPU时钟周期时间减去CPU地址输出传播延迟和CPU数据输入设置时间。
一些设计师使用交错来解决这个时序问题,其中缓存或主存储器的宽度是CPU数据总线的四倍,并使用复用来基于CPU时钟自动执行突发序列,而不是基于CPU地址输出。 (以这种方式交错的DRAM通常只需几个等待状态就可在周期开始时运行,因此称为4:1:1:1)。其他设计师在CPU和RAM之间放置突发计数器,以便可以减少时钟脉冲到突发计数传播延迟。今天最受欢迎的高速缓存数据RAM已经将计数器集成到芯片上。

在2.2.6节中,我们花了大量时间和流行语来描述线路缓冲区和早期继续使用(CPU允许作为多字线在高速缓存中更新时继续运行的方式)。查询正在设计的处理器的内部缓存是否使用早期继续使用可能会节省您一些时间和精力。作为案例,作者的一位熟人尽力设计了一个围绕MOTOROLA 68030的二级缓存,支持68030的突发缓存线路填充机制。奇怪的是,具有突发支持的设计表现不如没有突发支持的类似设计。为什么?因为68030的内部缓存不支持早期继续使用。为更好地理解该问题,请参见图2.25。如果所有指令都在最短的时间内执行,并且如果指令全部按照图中所示的方式行动,即在不干扰其他地址访问的情况下连续执行,则突发将不会比分离周期更快。然而,在很多情况下(可能有75%,因为线路长度为四个字),在处理器需要访问完全不同的地址之前,仅使用线路的一部分。采用68030的非阻塞设计,必须在CPU需要查找其他地址之前完成突发,因此在此争用期间停止CPU执行任何工作。
高速缓存设计中另一个困难点是产生良好时序的写脉冲,特别是对于允许在零等待状态下进行缓存写入周期的情况,无论是因为缓存采用了复制回写策略,还是因为写透设计使用了写缓冲区。高速写入周期存在两个问题的源头:偏移和噪声。

写脉冲偏移是由于使用单独的硅片来控制写脉冲和RAM的数据和地址输入引起的。如果写脉冲和数据、地址在同一块硅上由同一时钟生成,无论在温度、电压变化还是IC制造过程变化中,所有这些信号都会一起移动。无论环境如何,时序都非常精确。在将地址输入或数据I/O缓冲到高速缓存的系统中,或者使用不同的硅片生成缓存数据RAM的写脉冲而不是用于生成RAM的地址和数据输入的硅片的系统中,设计师必须考虑这些不同的集成电路可能来自不同的制造商,并且会表现出不同的传播延迟、不同的温度跟踪(更不用说每个芯片可能在任何时候都处于不同的温度下),以及所有可能导致数据、地址和写脉冲输入相对于彼此出现大幅波动的最坏情况。解决这个问题的常见方法是在缓存设计中使用同步静态RAM,因为同步SRAM不使用写脉冲,而是使用单独的时钟输入来同时采样地址、数据和写使能输入。我们在这里不会深入介绍同步SRAM,但是同步SRAM的写周期的时序波形如图2.26所示,很容易看出写周期可以有很多裕度,仍然可以以非常高的速度执行。同步SRAM还巧妙地解决了高速写脉冲的噪声问题。假设您已成功收集了适当的逻辑来精确控制写脉冲与缓存数据RAM的地址和数据输入的时序。在50 MHz的时钟频率下,由于印刷电路板的传输线效应变得显著,因此需要谨慎的电路板布局,并且需要使用阻抗匹配负载对写脉冲线的两端进行适当终止。尽管如此,每次追踪中的每个变化(如焊接点、器件引脚甚至追踪的一个拐角)都会产生反射,在将这些反射添加到原始信号之后,它们可能导致写脉冲出现振荡,即在写周期开始时不稳定地保持为高电平或低电平,并且在周期结束后继续存在或再次发生。这会引起困扰,因为写使能必须是精确时序的脉冲,而不是电压水平。同步SRAM将写使能输入视为一个可以在采样窗口之前经历无数次转换的电平,并且在窗口通过之后可以自由上下弹跳而不会产生影响。
2.5.3 Bus Turnaround
缓存设计中可能存在总线争用的主要原因有三个。第一个是如果缓存设计使用了交叉连接的RAM满足突发循环。第二个是在使用多路架构的情况下,缓存控制器必须在最短的时间内决定哪个RAM bank用于满足数据请求。第三个是在交叉连接数据和指令缓存以模拟哈佛架构的缓存中。在所有这些示例中,问题是如何在最短的时间内关闭一个RAM并打开另一个RAM。必须尽一切可能避免两个或更多RAM bank输出产生重叠,因为引起的争用会导致高功耗、地线噪音、射频干扰(RFI)、对RAM输出引脚的压力以及其他问题。
尝试以这种方式交错RAM的一个问题是,静态RAM的规格说明不足以确保不会发生总线争用。一些制造商通过保证最小和最大的开启和关闭时间来解决此问题,但这只是少数情况,而不是常态。因此,设计师通常被迫假设开启和关闭时间可以在零和规格所指定的最大值之间的任何位置。更大胆的设计师假设零延迟是不可能的,并且会找到一些使他们感到舒适的数值,但是最大和最小数值之间的差距仍然会减慢交错系统的速度,除非设计师选择忽略总线争用的影响(至少在需要调试系统之前)。
某些CPU使用单独的数据输入和输出引脚,这在封装引脚数量方面代价相当大,只是为了解决由总线切换引起的争用问题,当CPU停止输入数据并开始写入时。这种方法有助于解决将CPU的输出与缓存数据RAM的输出同步的问题。
Chapter3 CACHE MEMORIES AND RISC PROCESSORS
3.1 THE RISC CONCEPT
现今的管理咨询师和商业作家告诫美国企业要抛弃古老格言和过去的老套方法,尝试不同的思维方式。这正是IBM T.J.沃森研究中心在1970年代末做的事情。在那个时候,半导体工艺的进步提供了越来越多的集成境遇,人们欢迎它作为增加CPU复杂性的手段。旧架构提供了简单的构造,例如使用简单寻址模式进行加载、存储和加法。随着更高的集成度,程序员可以提供单个指令,对基于多个内存操作符的运算对象进行乘除或执行简单级数逼近函数的子集,使用高度精密的寻址技术。这种方法很大程度上增加了代码密度。 IBM的研究表明, CPU的大部分工作都是用来执行简单的读写功能,而不是使用复杂的寻址。这应该不足为奇。令人惊讶的是,加载和存储操作与其他任何操作的比例如此之高,以至于简化其他操作或添加更复杂的指令的优点与提高加载/存储指令速度的效果相比显得微不足道。各大学进行的进一步研究表明,如果编译器仅使用富指令集的子集,专注于执行速度最快的指令,无论它们的操作多么简单,都可以生成更快的代码。即使对于英特尔的CISC处理器系列,从Pentium开始也是如此。英特尔费尽心思地向软件公司提供优化编译器,以确保新软件充分利用处理器吞吐量。抛弃复杂的寻址模式!摆脱任何高级指令!只需加速加载和存储!让我们看一个数字例子。假设编译器生成的代码(基于跟踪)按以下比例执行:从简单地址加载40%,存储到简单地址40%,分支/跳转/推送/弹出5%,寄存器之间的操作3%。这应该看起来是完全合理和可信的,特别是对于那些在386时代,在昂贵的数学协处理器添加后,非常失望于其PC性能没有得到显著提高的人们。在这个例子中,如果可以将使用简单地址的加载和存储速度提高2.5%(1-39/40),则对更复杂地址的引用可以减慢两倍,程序仍然会运行同样快。
3.1.1 The CISC Bottleneck

那么谁在乎呢?CPU的设计者在乎!CPU的最大时钟速度由两个简单的因素决定:门传播延迟和关键路径上的门数量。所有处理器设计都有点循环性质。数据或指令必须在下一个时钟周期中按时环绕循环路径被时钟锁存到寄存器中。流水线系统通过在循环路径的中间点添加寄存器,并以更快的速率进行时钟锁存来加快这个过程(在某些情况下除外)(参见图3.1)。任何CPU的最小时钟周期时间由寄存器时钟之间的最大延迟长度路径设定。在CISC处理器中,更丰富的指令集的增加会逐步向这个关键路径添加门。
了解图灵机的人会意识到,通过使用一个非常简单的机器,可以解决各种各样的问题。如果CPU的速度是由涉及十个门的延迟路径决定的,并且如果我们可以通过精简一半的指令集来消除一个门,那么考虑到CPU刚刚变快了10%,去除这个门和它所支持的所有指令可能是值得的。根据刚刚展示的编译器统计数据,在不涉及复杂引用的所有指令中实现10%的改进,将使得复杂引用的计算时间最多延长六倍,而不会影响系统性能!在这一点上,显然很明显,与可能对所有其他指令造成负担的硬件实现相比,以更高的速度提供更复杂指令的软件替代方案实际上可能是一个非常好的权衡选择。
减小指令集计算机(RISC)的方法是基于同时减小指令集大小和周期时间的原则,找到最小化的指令集。与许多研究一样,这个过程最终并不客观,因此产生了几种RISC架构。不过,RISC处理器的指令集如此相似,以至于作者的一个朋友声称,如果读者从中间开始阅读处理器的指令手册,根本无法分辨是哪个处理器。所有的RISC CPU都专注于简单的加载和存储操作,在寄存器之间进行算术和逻辑运算。相反,CISC CPU通常允许将内存用作算术和逻辑运算的源和目的地,并且很可能支持一些相当奇特的地址模式。
3.1.2 The Rise Architect's Goal
在设计RISC处理器时的目标是使最常用的指令尽可能快地运行,虽然这会导致指令集复杂性的降低。相比较而言,CISC处理器通常会提供执行一个低级别指令子程序所需功能的指令,但执行这些指令可能需要多达30个周期。我们必须权衡减少执行代码的效用、增加代码密度的效果和更频繁遇到的指令的执行速度,就像在第3.1节的示例统计中所展示的一样。
RISC架构的重要目标是尽可能每个周期执行尽可能多的指令。为了实现这一点,设计师往往广泛使用流水线技术,甚至使用分支预测作为一种尝试在处理器实际请求指令之前获取适当指令的方法。在一种非常简单的分支预测形式中,当获取一条指令时,下一个内存位置的地址,极有可能是下一条将被使用的指令,已经被输出到内存系统。当然,还有更复杂的算法,但这种简单的分支预测对我们来说已经足够。
只要代码按线性方式运行,并且不进行主存数据访问,处理器确实可以在每个时钟周期执行一条指令。当对主存进行数据访问时,将消耗额外的周期,使得每条指令的周期数增加一部分(尽管通过允许处理其他不需要通过主存加载数据的指令,这可能有时对RISC处理器来说是隐藏的)。此外,当分支被执行时,由于简单预测单元选择的分支将不会被预测到,至少会浪费一个周期,直到输出地址从合理的估计值变为CPU所需的实际地址。这有时可以通过允许在分支指令之后执行指令来解决。
RISC处理器往往不支持像跳转到子程序这样复杂的指令,因为在加载目标地址时,会消耗多个周期,并且在将程序计数器的先前内容推送到堆栈(通常位于主存中)时也会消耗多个周期。这将导致每条指令所需的时钟周期数增加,这在RISC处理器的世界中是不可接受的。相反,子程序往往将程序计数器放入寄存器中,并让编译器处理上下文切换的细节。
3.1.3 Interaction of Software and Hardware
RISC架构的整体方程中,软件起到了相当大的支撑作用。一些RISC销售支持程序甚至提供了具有不同缓存配置的系统模型,以便设计者可以在确定系统构建方式之前,测试特定代码在各种缓存策略下的性能。当然,如果我们在前面的示例中放弃了复杂的引用,并将其替换为配置不良的代码片段,那么所有这些都无法协同工作。为了从高速硬件中获得最大的速度,软件需要充分利用支持的特定处理器。
整个RISC解决方案不仅仅依赖于对硬件的优化(再次基于统计数据的测量),还依赖于使用优化编译器,进一步将编译后的代码的统计数据朝着加速处理器吞吐量的方向倾斜。
3.1.4 Optimizing Compilers

编译器如何优化代码,这对缓存性能有什么影响?通过阅读本书,您应该意识到理解软件与硬件整体性能之间的相互作用的重要性。通过了解您特定的优化编译器如何工作,您将更好地能够优化缓存策略。在第3.1.2节中,简要讨论了分支。每当取一个分支时,处理器通常需要消耗一个额外的时钟周期,以便程序计数器有足够的时间重新加载自己。这意味着任何循环都会比运行相同的几个指令重复一次的线性代码运行得更慢。速度提升的百分比与循环长度成反比。优化编译器将检查使用固定重复次数的循环,确定如果循环被展开,代码会变得过大,评估保留循环和展开循环(有时称为展开)之间的速度折衷,基于循环长度,然后决定使用循环还是高度重复的线性代码(见表3.1)。
类似地,优化编译器将决定是否将子例程保留为子例程或将多个副本嵌入到调用它的少数代码位置中。子例程调用不仅由于重新加载程序计数器而具有延迟,还可能需要大量的堆栈支持来保存和恢复上下文,导致高延迟,通过这种优化过程可以完全消除高延迟。
RISC体系结构更加关注寄存器,并具有富有寄存器的CPU。这意味着支持这些CPU的编译器将专注于将频繁的内存访问转换为寄存器访问,结果是外部数据传输与指令调用数量比例失衡。
除了使反汇编代码难以跟踪之外,所有这些对缓存设计者意味着什么?首先,使用直线代码而不是小循环倾向于使更长的线路长度成为一个更合理的选择,因为在这种优化的代码中很可能进行更高数量的顺序访问。其次,在第1章和第2章中,我们探讨了对于由于子例程调用及其随后的堆栈操作而产生的冲突而遭受抖动的系统,更高联想性高速缓存的重要性。如果将几个子例程拉入调用例程中,将会有较少的地址冲突和堆栈访问,从而稍微降低使用更高缓存联想性带来的改进。再次强调,代码将更加内嵌,而不是被分成子例程,因此较长的线路将比使用非优化编译器时更可行。第三,如果大多数时间都在寄存器之间传递数据,并且通过在寄存器丰富的机器上使用优化代码,数据加载和存储显著减少,则设计者应该更加注重发挥指令缓存的作用,而不是数据缓存。最后一个观点得到了加强,即对于小指令集的优化代码,其大小将明显大于非优化的CISC等价物。
3.1.5 Architectural Trade-offs
处理器架构师必须在速度和其可能提供的许多功能之间进行权衡。如果要最大化每个时钟周期的指令数,指令、操作数和目的地必须同时对CPU可用,而不是按顺序访问。此外,一些大的延迟可能会发生在内存管理单元内。这些权衡的方法决定了缓存设计的两个重要方面,两者都已用于某些RISC处理器设计中以减少门延迟路径。
3.1.5.1 Harvard vs. Von Neumann

冯·诺伊曼体系结构如此普遍地使用,以至于哈佛体系结构和冯·诺伊曼机器几乎听不到这些术语。简单来说,虽然冯·诺伊曼机器有一个单一的地址空间,其中任何部分都可以作为指令或数据访问,而哈佛体系结构使用两个独立的固定大小空间:一个用于指令,一个用于数据。这自动意味着哈佛机器可以在单个周期内加载或操作数据,并通过指令总线从指令空间获取指令,数据通过数据总线加载或存储到数据存储器中(图3.2a)。也暗示了冯·诺伊曼机器(图3.2b)必须在单个内存空间的单个数据总线上,在两个单独的周期中获取指令,然后加载或存储操作数。当然,任何追求性能的机器都会尝试减少需要对主存储器进行两次访问的指令数量。在任何类型的CPU中获得惊人的速度优势的一种方法是允许同时访问指令和数据。有两种基本方法可以做到这一点。第一种方法是设计具有富有寄存器的冯·诺伊曼体系结构,其中大多数指令作用于两个寄存器中包含的数据,而不是存储在内存中的数据。在寄存器丰富的RISC机器中,访问指令,对芯片寄存器中间的数据执行某些功能,并将数据写回到这些寄存器之一,全部在单个周期内完成。(实际上,处理器的内部流水线将任何一个指令分散在几个周期中,但由于同时操作多个指令,所以有效的指令执行时间不到一个时钟周期。)寄存器丰富的体系结构的优化编译器会尝试确保最常使用的操作数将映射到寄存器(寄存器供应短缺),而很少使用的操作数则永远不会映射到寄存器,而是驻留在主存储器中。通过这种方法,大部分数据访问将被隐藏在总线之后,总线活动的大部分将围绕指令获取,强制访问比在将指令和数据操作数更随机地交织的机器中更加顺序化。这直观地意味着长的线路可能是这样的机器非常好的缓存策略,并且一个单独的指令缓存可以提高吞吐量,就像一个组合缓存一样。

其他RISC处理器使用冯·诺伊曼风格的统一内存空间作为主存储器,但将缓存分为指令和数据空间,使得CPU缓存子系统呈现出哈佛架构的特征(见图3.3)。数据和指令同时来自两个不同的存储器,通过两个不同的总线传输。一个单独的内部累加器提供一个操作数并存储结果。现在,指令获取和数据加载/存储可以在同一个周期内进行,但整个系统仍然具有冯·诺伊曼机器的主存储器灵活性。这种方法使得处理器的性能不再依赖于CPU的寄存器集的大小。
哈佛架构具有两个优点。首先,编译器在管理寄存器内容以确保数据很快且频繁地被使用方面需要更少的注意。其次,在具有高级流水线的寄存器丰富型机器中,可能存在后续指令需要早期指令产生的数据,而这些数据尚未从流水线中传出的可能性。通过计分板技术来处理这些问题,该过程使用特定的标志位来表示寄存器中的数据是否是最新可用的。这个过程的实现较为复杂,并且当一条指令必须等待其操作数可用时,会导致延迟。
3.1.5.2 Physical vs. Logical Caches

回顾一下,在2.2.1节中,我们讨论了逻辑缓存和物理缓存的属性。在RISC设计中,按照减少门延迟的RISC理念,只使用逻辑缓存似乎是合适的选择。你可能还记得逻辑缓存位于内存管理单元的处理器侧,其延迟较小,而物理缓存位于MMU延迟的下游(图3.4)。
一些RISC处理器通过增加流水线的深度来解决MMU延迟问题,从而隐藏了对缓存的延迟。这可能对设计师提出了挑战,但简化了一个我们将在第4章遇到的问题,即在逻辑缓存中维持一致性。
3.2 FEEDING INSTRUCTIONS TO A RISC CPU
设计一个快速的CPU,然后让它等待慢速的内存接口是不负责任的。因此,我们要在RISC处理器和缓存之间进行竞争。甚至有一位处理器架构师建议在设计处理器之前先为缓存设计最佳速度和命中率。这似乎有点极端,因为如果你不知道编译器将生成什么样的代码,很难优化缓存的命中率,而且编译器将根据处理器的架构进行优化。最重要的是,整个系统必须作为一个整体来设计,每个部分都会影响其他部分的设计。即使是一个出色的缓存,如果与糟糕的主存储器相配合,也可能比一个缓存不足但设计非常良好的主存储器的系统性能差。
与任何CPU一样,RISC处理器大部分主存储器的访问用于获取指令。因此,那些将大部分注意力放在优化指令访问上的设计师将设计出最佳的缓存。缓存设计师可以借鉴RISC处理器架构师的经验,简化设计以减少门延迟。大多数RISC处理器的设计者主张在芯片上放置一个小型的一级缓存和一个较大的二级缓存,而不是一个单独的较大缓存,这样一来一级缓存可以用最少的门延迟来构建。SRAM越小,地址译码器中的门延迟就越少。
3.3 PROBLEMS UNIQUE TO RISC CACHES
如果高速缓存无法满足处理器对数据的贪婪需求,整个RISC哲学就会崩溃。要记住的是,RISC的假设是通过减少关键时序环路中的延迟路径来加速处理器的时钟频率。缓存是这个环路的一个组成部分。设计一个非常快的处理器有什么用,如果在每个单独的指令周期中都要等待缓存访问延迟呢?
在高时钟频率下支持零等待击中可能会带来很大的问题。简单地说,进入和离开一个硅片通常需要约3纳秒的时间(假设使用TTL I/O电平)。由于缓存RAM和CPU通常是两个不同的硅片,因此6纳秒可以在芯片间通信中消失。即使其余延迟总共只有1纳秒,这样系统的最大时钟频率也只有166MHz,已经低于大多数当前处理器的时钟速度!
为了解决这个问题,处理器设计师总是在CPU芯片本身上放置至少一个缓存。这么做有一个相当好的理由,因为即使是小缓存也往往具有相对较高的命中率。然而,这并不免除缓存设计师的速度问题。在写作本文时,RISC二级缓存设计的问题仍然非常棘手,通常需要使用极高速的同步SRAM。
当然,在RISe缓存中,与2.5.1节中提到的相同的引脚负载问题变得更为严重,特别是在RISC CPU现在提供的高时钟频率下。降额,在较低时钟频率下可以忽略不计,但在更高的速度下就成为一个决定性因素了。大多数架构采用的方法是使用最广泛的设备和最先进的密度来实现缓存数据RAM。通过减少由CPU地址总线驱动的设备数量,可以避免电容负载的有害影响。
Chapter4 MAINTAINING COHERENCY IN CACHED SYSTEMS
经常引用的一项统计数据表明,夫妻间最常见的争吵原因是金钱问题。为什么这样的统计数据会出现在一本关于高速缓存设计的书中呢?因为某些金钱争论和缓存一致性问题之间存在着深刻而又务实的相似之处。
让我们假设大多数金钱争议都以类似以下例子中的一种开始:
"'你为什么不在支票登记册上记录当你从自动提款机取钱时的支出?现在我们反弹了三张支票!"
"你在Visa账户上刷了900美元,却忘了告诉我?难怪在餐厅里当着我老板的面我的信用被拒绝了!"
"那是我们准备去夏威夷的钱!"
"那100美元现金到底发生了什么事情?"
这些例子展示了金钱的管理和沟通问题,而同样的问题也存在于缓存一致性的议题中。无论是金钱还是缓存,都需要在不同的操作之间保持一致性和准确性,否则就会导致混乱和冲突。
在所有这些情况中,都涉及到由婚姻中的双方共同拥有的金钱(或信用),而其中一方在未告知另一方的情况下进行了支出。如果我们从夫妻转移到计算机,并将共同财产转换为内存空间,我们可以看到,如果两个不同的设备在没有相互通知的情况下操作相同的数据,可能会发生潜在的灾难。夫妻中的每个成员都认为自己对银行账户中的内容或信用卡的已消费金额有一个心理画面,就像缓存内存应该包含主存储器中相应内容的准确副本一样。当夫妻中的一方修改主存储器中的内容时,必须通知另一方。同样地,当系统总线上的任何设备更新主存储器或稍后将被复制到主存储器的缓存位置时,它必须保证没有其他设备会对主存储器中数据的新鲜度产生误解。
这些问题已经在许多软件数据库中得到了解决。想象一下订购航班座位的问题。联合航空公司拥有一个数据库,仅在美国的三个办事处,大约有2100名电话订票代理人可以同时访问该数据库。同样,美国的数百名旅行社也可以在线预订航班座位。在最糟糕的情况下,从纽约飞往芝加哥的航班可能只剩下一个座位,在同一时间内,每个使用系统的人都试图将该座位出售给客户。如果问题没有得到解决,可能会有大约3000人被分配到同一个飞机上的同一个座位!
缓存一致性是确保多个缓存系统中缓存内存的内容与主存储器的内容要么相同,要么受到严格控制,以便不会混淆过期和当前数据的问题。就像任何其他缓存术语一样,与一致性这个词相比,还有很少使用的替代词,即一致性和实时性。过期数据这个术语用来描述那些不再反映所代表的内存位置的当前值的数据位置。当我输入这本书时,新版本的本章节存储在计算机的主存储器中,而旧版本则存储在计算机的硬盘上。下次我保存文件时,磁盘和主存储器将包含相同位置的相同数据,并且在我再次开始输入之前它们将是一致的。
4.1 SINGLE-PROCESSOR SYSTEMS
初看起来,大多数设计师会认为一致性只是多处理器系统中的问题,或者可能是具有写回缓存的系统中的问题。似乎在单个CPU系统中使用写通方式的缓存永远不会有一致性问题。但这是不正确的,因为存在着处理器无法控制的活动,换句话说,就是输入和输出活动。最简单的例子是内存映射的轮询式I/O设备,处理器会不断读取这个设备以确定一个位的状态。我们在2.2.7节中使用了相同的例子来说明非缓存区域的使用。如果缓存中包含内存映射的I/O位置的副本,并且CPU引用的是缓存中的副本而不是I/O设备,那么处理器将永远无法看到I/O位状态的任何变化,因为它将读取缓存中陈旧或不一致的值,而不是从内存映射的I/O位置中输入的真实值。
通过将I/O位置映射到不可缓存的地址,可以很容易地解决这个问题。当另一个总线主设备可以在没有CPU干预的情况下向主存储器写入时,问题就变得棘手了。主设备是指可以命令主存储器执行读写周期的任何设备,而不需要CPU为该设备执行这些周期。在单处理器系统中,最典型的例子是像磁盘控制器或视频接口这样的DMA设备。
以磁盘控制器为例,想象一下,当整个缓存已经存满了主存储器地址的副本时,CPU启动了一个DMA传输,将程序的一部分从硬盘传输到主存储器。其中一些缓存位置无疑是将被传入的DMA数据覆盖的主存储器位置的副本。如果这些缓存位置与主存储器的内容不同步,新提取的代码将在所有未缓存的地址上执行,并夹杂旧程序的缓存副本。更糟糕的是,在写回缓存中,从主存储器到DMA设备的输出也可能会引发问题,因为CPU认为它发送给DMA输出设备的数据是最新的,但如果某个数据块的一部分仍然存在于缓存中,并且是"脏"状态,那么某些陈旧的数据将最终进入永久存储器中。这两个问题将在本节的后续部分进行讨论。
也许这是一个区分缓存策略和缓存/总线协议的好地方。"协议"是缓存架构师用来表达缓存、处理器、主存储器和另一个总线主设备之间通信方式的术语。缓存策略确定了缓存与CPU和软件的交互方式,并设置了缓存的命中率。协议是系统中所有子系统保证一致性并避免总线冲突的手段。在本章中,所有的一致性机制都必须围绕缓存策略和总线协议进行设计,或者总线协议必须围绕缓存策略和一致性机制进行设计。
4.1.1 DMA Activity and Stale Cache Data
在DMA传输过程中,当前总线主设备(DMA设备)可以写入可能也存在于缓存中的主存储器位置。缓存控制器必须能够确保缓存内存的内容与复制的主存储器位置保持一致,而不是包含之前主存储器的副本。最简单的方法称为缓存清除,每次进行DMA写入周期时,就会使整个目录无效。完成这一操作的三种最常见方法如下:1)使用特殊的无效化硬件,在每个缓存行的有效位中写入无效状态;2)使用具有硬件复位功能的特殊缓存标签RAM执行相同操作;3)重置主要的有效标志,该标志将缓存标签比较器的输出传递给CPU(假设设计中没有使用有效位)。关于这种方法,唯一可以说的好处是它很有效且易于实现,只要缓存是简单的写通实现即可。但最糟糕的是,它需要在每次DMA写入周期后重新填充缓存。在某些情况下,这并不那么糟糕。在运行DOS的PC中,CPU在DMA块传输期间总是停止工作,因此在几个后续的缓存行填充周期中额外的延迟并不明显。而在使用类似UNIX的多任务操作系统的系统中,刷新操作可能会造成灾难性后果,因为当第一个任务需要DMA活动时,操作系统会尝试将另一个任务调度出来。理想情况下,另一个任务应该在缓存中执行,以使DMA活动和处理器活动彼此不干扰。
另一种更高效的确保DMA传输一致性的方法是提供硬件,监视所有系统总线周期并检查其地址,以便在其中一个地址影响到缓存时警告缓存。
总线观察或窥探机制是一种简单的方法,可确保任何与主内存更新或清除相关的系统总线周期更新或清除相应的缓存位置。在主内存写入周期中,如果被寻址位置在缓存中有副本,或者在一些设计中,如果该位置仅可能被包含在缓存中,它将被覆盖或无效,具体取决于设计。用于处理窥探写击时失效线的术语是反向失效,这意味着失效正在发生的方向与缓存更新的正常方向不同。大多数设计人员称其为缓存观察系统总线,并通常使用窥探术语,而其他人则认为系统总线正在查看缓存,称该过程为询问或说系统总线询问缓存的内容。另一个不那么广泛使用的术语是交叉询问,听起来像律师描述当前总线主控寻找其他缓存中可能存在的一致性问题的方式。查询和询问术语的问题在于它们暗示请求设备在查看主内存之前首先从任何现有缓存中请求数据。在真实的设计中,缓存通常观察总线,并在可能出现一致性问题时采取行动。就像里面有人在外面看,而另一个人在外面看里面。在一个回写缓存中,窥探逻辑还必须注意从缓存中的脏单词而不是主内存中的脏单词满足的主内存读取。DMA设备,通常是磁盘,将需要发送最新的内存地址副本,而不是实际包含在主内存中的数据。在窥探缓存设计中,缓存标记RAM持续监视主内存总线活动。这可以通过保持相同副本或超集的缓存实际目录(稍后详述)的独立缓存标记RAM执行,或通过缓存的实际目录执行。使用缓存实际目录的设计分为两类。最常见的类别是缓存在主内存总线和处理器之间进行复用。这通常称为双端口目录或双端口标记。在某些设计中,窥探周期通过停止CPU开始,从而禁用其对缓存的访问。然后将DMA地址路由到缓存标记RAM,并在DMA写入主存储器的情况下,如果有DMA命中,则可以比较并写入无效位置,比较并保留有效位置,并将新数据写入缓存数据RAM(如果再次命中),或仅使其失效(无论是否存在命中)。选择这些替代方案是系统相关权衡的另一个方面,因为前两个需要读写周期,这会导致CPU被禁用更长时间,但是最后一个最快的替代方案肯定会在许多无辜的非匹配缓存位置上出现问题,然后需要在随后的访问周期中更新,再次消耗CPU速度。就像每个其他缓存决策一样,对于这三种哪个最适合您的系统,没有简单的答案。所有这些都取决于缓存的大小和联想度,可能在很大程度上取决于运行在机器上的软件结构。其他方法涉及以使窥探周期对CPU不可见的方式复用标记RAM,或将窥探地址交给CPU并请求CPU本身通过专用硬件执行后勤,如大多数处理器所做的那样。复用方法不会减慢CPU速度,但只能在处理器在总运行时间的显着百分比内放弃总线时使用。基于CPU的失效机制可能会导致处理时间显著延迟,因为每个失效通常会消耗几个CPU周期。


一种离散缓存实现中的复用缓存标记RAM方法可以节省一些开销,并且不会减少芯片数,因为它需要在系统总线和本地总线之间的缓存标记RAM的地址输入上添加多路复用器(如图4.1)。由此带来的额外逻辑延迟可能会导致设计无法跟上快速CPU的步伐。在大多数处理器芯片上的综合缓存实现中,这种方法确实是有意义的,因为多路复用器可能已经在关键路径上,并且只需要扩宽,即使没有,它也可能以不到一个时钟周期的速度惩罚实现缓存标记RAM的访问时间。 这种方法的变化称为DMA通过缓存,其中DMA设备基本上与CPU /缓存总线连接,而不是与主存储器总线连接(如图4.2)。在DMA期间,处理器与CPU /缓存总线隔离,而DMA设备读取和写入缓存和主存储器的方法与CPU使用的方法完全相同。即使缓存控制器也不知道正在进行什么样的DMA。虽然这会关闭CPU在DMA期间的持续时间,但当DMA结束时,缓存是一致的,并且可能包含至少几个有用的位置,具体取决于选择的写入未命中策略。在这种类型的写通缓存中可能使用的最佳写入策略是,在写入未命中时不要替换行,因为1)该行可能存在有用的代码或数据,而处理器在DMA之后需要它们,2)在DMA转移期间,通常会传输很多暂时不使用的数据。如果DMA将地址的经常使用的缓存副本覆盖为不太可能使用但恰好位于某些有用信息的相同扇区的内容,则会出现问题。 很容易看出为什么一些设计人员将这种方法称为读取通缓存,尽管此术语实际上只讲了一半故事。 DMA通过缓存方法的一个有趣的附带效益是,可以完全避免有效位。 如果DMA数据是唯一缓存的数据(即ROM,EPROM,I/O位置等映射到不可缓存空间中),则缓存仅在自上电以来没有接收到DMA数据的那些地址处包含随机数据。 哪一个更糟糕:从DRAM还是从缓存中执行随机位?其实没有什么区别。 只要代码调试到只访问合法数据(源自硬盘而不是随机上电位),那么这些数据可以由缓存或主存储器提供,唯一的区别是执行速度。
第二类嗅探机制使用重复标记来嗅探主存储总线。根据缓存设计的不同,这些机制可以非常简单、非常昂贵,或者完全免费但很难理解。让我们从最简单的开始,逐渐过渡到最复杂的机制。
双缓存标记或双目录系统在相似的芯片数量下提供了更高的操作速度、异步操作和更大程度的系统设计灵活性,但需要稍微更昂贵的组件。

在双缓存标记RAM系统中,有两个相同的目录:一个监视CPU的地址总线,另一个监视系统地址总线。本地缓存标记RAM的内容的副本会复制到系统缓存标记RAM中。初步猜测会认为需要做出特殊的努力来确保嗅探标记RAM包含与缓存标记RAM相同的信息。实际上,这是一个微不足道的问题。在任何缓存行更新(图4.3)期间,缓存地址总线连接到主存储器总线,因此在更新缓存标记RAM时,嗅探标记RAM和缓存标记RAM将同时看到相同的地址。然后,只需使用相同的写脉冲来更新嗅探标记RAM和更新缓存标记RAM。
当CPU正在从缓存中操作,而另一个主写操作发生时,系统总线地址将与系统总线缓存标记RAM中的地址进行比较。如果存在DMA写命中(意味着正在访问的主存储器地址已经被复制到缓存中),则嗅探标记RAM会注意到匹配,并导致缓存控制器停止CPU并使匹配位置无效或更新。此时应同时使嗅探标记RAM和缓存标记RAM条目无效,以确保后续对先前无效位置的嗅探命中不会发生。甚至有些系统在完整的缓存之外还使用了一个嗅探标记RAM,但在嗅探命中时通过其复位输入引脚冲刷嗅探标记RAM和目录的缓存标记RAM。虽然这是一种激进的方法,但与每个DMA写周期都冲刷目录的系统相比,该系统经历的缓存冲刷次数更少。令人惊讶的是,在嗅探命中时进行冲刷的方法被使用了,考虑到对于这两种一致性机制,缓存控制器的复杂性几乎是相同的。
在进一步讨论更复杂的一致性机制之前,这可能是另一个好地方,来谈谈我在本书中最喜欢的话题之一:选择缓存策略时需要考虑系统上运行的软件。一致性方法的复杂性不应该超出正在使用的操作系统所需的范围。再次以基于Intel的单处理器个人电脑为例,并且只考虑那些仅使用DMA进行磁盘I/O而不做其他事情的系统。如果设计仅针对MS-DOS或Windows 3.1应用程序,那么在缓存设计中,嗅探可能不是一个重要的特性。在较简单的操作系统中,CPU在DMA访问期间不允许操作,因此在CPU空闲时间内减少向缓存的失效周期不会改善系统性能。另一方面,在使用UNIX或其他更复杂的操作系统和多处理器系统中,通过减少不必要的失效周期可以实现显著的性能提升,因为在这些情况下,处理器与其他总线主设备同时操作。
在升序复杂度方案中的下一个一致性机制涉及包含原则。这种设计用于嗅探标记较大,不一定与缓存目录的关联性相同的情况。首先,让我们看看为什么在理性的人会尝试这样做,然后让我们探讨它是如何工作的。
假设您正在构建一个用于处理器内部缓存的外部嗅探机制,该缓存具有8KB、四路、集合关联的统一物理缓存,并且每行大小为四个字(16字节)。似乎最容易的事情是复制内部缓存的目录。需要多少芯片?带上我们的数学帽子,我们可以发现需要四个单独的缓存标记RAM,每个都对应四路缓存之一,而每个缓存标记RAM则需要具有[8K字节/(4路·每行16字节)] = 128个位置的深度。假设有30个地址位,其中28个用于寻址缓存行,并且由于7个位用作集合位以进入128个标记位置(128 = 27),其他21个位将是标记位。加上一个有效位,总体缓存标记RAM需求将是四个128 X 22位的嗅探标记RAM。如果我们使用目前市场上最广泛的集成缓存标记RAM,它采用8K x 8位的组织方式,则需要12个设备来实现标记(22位/8 = 3,乘以四路缓存)!通过使用包含机制,可以将其简化为两个8Kx 8集成缓存标记RAM,在一个简单的直接映射配置中连接起来。我们来看看这是怎么实现的。
如果嗅探标记RAM始终可以包含缓存目录的超集,则嗅探标记RAM可以用于在这些周期传递到实际缓存之前验证所有失效周期。这样,嗅探标记RAM可以从缓存中筛选出大量无效的失效尝试(以及相应数量的不必要CPU等待状态)。"包含"一词表示整个缓存目录的内容包含在嗅探标记RAM的内容中。
将snoop-tag RAM强制成为更复杂缓存目录的一致超集可能一开始看起来很困难,有几个非常好的原因。首先,具有比snoop-tag RAM更高关联度的缓存能将主内存位置的副本放入多个缓存位置,而关联度较低的snoop-tag RAM将受到限制,在某些情况下无法同时包含易于适应更高关联度缓存目录的一组地址。其次,缓存可能不向外界披露足够信息,以使snoop-tag RAM确定正在被更新的哪一行。大多数处理器不会告诉外界正在替换哪些内部缓存行。如果缓存目录和snoop-tag RAM彼此难以理解,它们如何保持子集和超集的状态呢?答案出奇地简单:包含。包含有两个基本原则:
-
写入缓存目录的任何标记项同时也写入snoop-tag RAM。
-
从snoop-tag RAM中移除(无论是通过总线监听还是替换)的任何标记项同时也被强制从缓存中移除。
细心的读者会立即注意到,在某些处理器设计中,可以在不更新snoop-tag RAM的情况下从缓存中删除数据。此后,snoop-tag RAM将可能在缓存中不存在的地址上经历监听命中。这就是snoop-tag RAM成为缓存目录超集的原因。
上述两条规则没有提及的另一个问题涉及将地址标记放入snoop-tag RAM但不放入缓存的情况。如果snoop-tag RAM的线大小大于主缓存,这可能会发生。同样,此时snoop-tag RAM仅仅成为缓存目录的超集。
即使缓存是四路设计而snoop-tag RAM是直接映射的,四路缓存也不能包含snoop-tag RAM中未复制的地址,因为在将地址添加到缓存的同时,其地址被放入snoop-tag RAM。如果在snoop-tag RAM中使无效的每个地址也在缓存自身中使无效(无论它是否仍然存在于缓存中),那么在snoop-tag RAM中突然缺失的缓存中就不会剩下任何东西。
现在我们已经确定了包含确实可以保证缓存目录的内容是snoop-tag RAM的子集,我们应该考虑一下其缺点。
可能最重要的一个缺点是,在使用包含的系统中,缓存永远无法保持满。不可避免地,由于无法在关联度较低的snoop-tag RAM内放置它们,本来可以继续驻留在缓存中的行将被强制退出。这导致缓存的命中率稍微降低,并且如果需要再次获取使行无效的线,则会导致从主内存中重新获取此行的时间损失。因此,确保snoop-tag RAM比缓存目录更大,以减少它们自身和随后被强制从缓存中移除的行数是一个很好的原因。另一方面,如果选择了过大的snoop-tag RAM,将会传递更多错误的使行无效周期给缓存,从而降低系统速度。没有免费的午餐!(顺便说一句,我们还没有考虑snoop-tag RAM可能有多小。信不信由你,snoop-tag RAM可以比缓存标记RAM更小,并且包含仍然可以工作。如果只允许驻留在缓存中的项具有在snoop-tag RAM中复制的地址,那么在这种系统中使用的缓存标记RAM的大小只会比snoop-tag RAM的一小部分被使用。这样会工作,但会对未使用的缓存部分造成可耻的浪费。)
第二个明显的缺点是包含可能会大幅增加传递给缓存的使行无效周期的数量。每当一个地址从snoop-tag RAM中移除时,就会发生缓存使行无效周期。无论被移除的地址实际上是否仍然存在于缓存中,以及snoop-tag RAM条目是由总线监听周期还是由导致snoop线被覆盖的缓存行更新使之无效。尽管看起来不好,但这实际上是包含的一个强大优势。
大多数缓存使行无效周期将由snoop-tag RAM中有效条目的替换触发。这只会在缓存未命中时发生,并且在缓存未命中周期中,CPU被停止并且无法继续执行,直到接收到新数据为止。由于snoop-tag RAM中正在替换一个条目,所以在新数据对CPU可用之前会经过几个周期,这些浪费的时间可以用来在缓存内部使一行无效。通过这种方式,大多数缓存使行无效周期被迫与CPU停止的时间重合。只有那些通过snoop-tag RAM筛选的总线监听周期才被允许停止CPU,以引起性能受威胁的使行无效周期。
如果系统中使用二级缓存,则可以使用包含(inclusion)来允许二级缓存对总线流量进行预筛选,并限制传递给一级缓存的错误使行无效周期的数量。在非常小的成本下,二级缓存的标记可以得到第二个用途作为窥探标记。通常只需要微不足道的额外逻辑来支持使用现有二级缓存的包含。从主缓存/CPU子系统输出的集合位将包含相同的值,该值将驱动进入主缓存以使主缓存行无效,因此需要一个集合地址锁存器。这个锁存器可以吸收到现有地址缓冲器的低位中,因此在芯片计数方面的额外成本通常为零。包含所需的标记位从二级缓存的目录中提供。通常只需要添加很少的附加条款到缓存控制器状态机中,因此缓存控制器的复杂性并没有显著增加。
脏包含与上游和下游缓存均使用复制回写策略的缓存层次结构中的包含类似。使用脏包含时,一个二级复制回写缓存会包含对应于主缓存中每个标记脏的行的标记脏行;但是,并非所有标记脏的二级行都必须在主缓存中标记为脏。这使得可以将一行从主缓存中驱逐而不会导致在二级缓存和主内存之间的接口上发生任何写流量。直到二级缓存需要该行来接收新数据,二级行才会被驱逐出去,并且二级行更新次数比主行更新次数少,因此流量保持在低水平。使用脏包含的另一个优点是,复制回写缓存中对标记脏行的窥探读命中必须从缓存中而不是从主内存中支持。如果二级缓存要在允许使行无效周期通过传递到一级缓存之前预先筛选总线写周期,则也有意义允许二级缓存预先筛选读周期。如果每个二级行的状态与主线的状态相同,则可能会发生这种情况。这并不意味着二级缓存中的脏行将始终包含与主缓存中的脏行相同的数据。要实现这一点,主缓存必须是写通关的,并且必须每次CPU更新脏行时更新二级缓存。相反,将二级行的状态更新为脏状态,当主行首次变为脏行时,并且在窥探周期中保持该状态,即使在窥探读命中事件中,也无法保证来自二级缓存的数据是一致的。因此,所有对脏二级缓存行的窥探读命中都必须发送到主缓存。尽管如此,这仍然比将所有总线读周期发送到主缓存更好。
4.1.2 Problems Unique to Logical Caches
逻辑缓存中遇到的最大难题可能是地址别名问题,这在第2.2.1节中首次提到过。这个问题源于两个单独的虚拟地址可能被映射到同一个物理地址的事实。换句话说,操作系统可能会将具有相同偏移地址但具有不同页地址的两个虚拟地址映射到同一页内的同一个物理页,因此这两个虚拟地址将共享同一物理页内的同一偏移(即同一物理地址)。这是多任务系统中任务之间通信的一种方式(例如应用程序中的系统调用)。
在任何DMA写入主存储器时,也必须更新或使缓存中与DMA设备写入的地址副本无效。如果两个虚拟地址映射到同一物理地址,则可能会存在两个缓存中的同一主存储器位置的副本。当另一个总线主机写入主存储器时,这两个副本都将变得陈旧。
当然,有很多方法可以解决这个问题,其中最不成熟的方法是在每个DMA写周期上刷新缓存。更优雅的解决方案涉及不允许缓存使用任何CPU输出地址作为MMU页面号位中的集合地址位。虽然这在许多情况下对缓存的大小产生了严格限制,但它完全消除了非统一缓存设计中仅表示两个不同逻辑地址的同一物理地址的两个并发缓存副本的可能性。思考一下。只有当两个单独的逻辑地址具有相同的偏移位但具有不同的虚拟页数时,它们才能映射到同一个物理地址。一个使用某些页面号位作为集合位的大型缓存可能能够维护同一个物理地址的两个不同副本,位于两个不同的集合地址中,但其集合位被限制为偏移地址的子集的缓存永远不会有两个相同的物理地址的副本,因为它们都将最终被映射到同一集合地址。

应用该方法的另一个规则是在窥探逻辑缓存设计中仅使用统一的直接映射缓存。在DMA期间,DMA地址的集合位可以输入到缓存标记RAM的窥探机制中,并且标记输出该集合地址的虚拟页数(见图4.4)。这只能在MMU两侧的集合位相同时才能工作,符合要求需要集合位是页面偏移地址位的子集。然后调用MMU将标记的虚拟页数输出转换为物理页号,然后将其与DMA的标记地址位进行比较。如果发生命中,那么该缓存行将被使无效。如果听起来很慢,那是因为它确实很慢,但想想如果在非统一的缓存或多路设计中,两个窥探标记必须一次一个地通过MMU处理它们的标记位需要多慢!另一种选择是使所有具有匹配集合地址位的项无效,但这可能难以实现,并且会不必要地使不成问题的缓存项过多失效。几乎不可能以CPU不停顿进行干净的窥探失效过程,因此逻辑窥探缓存设计通常保持简单,以避免在窥探周期中产生大量速度惩罚。
虽然我从未见过这样的机器,但我不会怀疑有些机器采用了反向转换方案,将物理地址映射回所有相关的虚拟地址,然后允许高度关联的缓存的多个路通过窥探新创建的虚拟地址。这将使逻辑缓存可以是非统一的或更具关联性,并且还将消除由要求集位是页偏移位的子集而引起的缓存大小限制。我只是希望我认识的人没有被负责设计这样的怪物。
关于地址别名问题的讨论通常集中在上下文切换上,上下文切换是描述DMA活动可能发生的时候的软件方式。对于别名的大多数关注都集中在缓存和非缓存系统中的上下文切换上,因为MMU对主存储器和磁盘的映射与缓存与主存储器之间使用的映射非常相似。如果你有机会与构思你的操作系统的软件设计者讨论你的缓存设计,不妨去做!你们两个都会惊讶地发现你们有很多共同的问题,而且你们可能会扩大彼此的视角。
4.2 MULTIPLE-PROCESSOR SYSTEMS
在紧密耦合的多处理器系统中,特别是那些任何处理器都可以在内存的任何部分执行任何任务的系统中,缓存一致性的问题非常重要。为了复习一下,我们将再次解释紧密耦合和松散耦合的词汇,因为它们最初是在第1章中定义的。紧密耦合的多处理器系统被设计为允许每个处理器均能同样地访问主存储器。而松散耦合的系统由两个或更多的处理器组成,每个处理器都有私有的内存空间。图4.5展示了这两种系统的示例。在松散耦合系统中,处理器之间的连接可以是共享的主存储器部分、先进先出队列(FIFO)、双端口内存,甚至是类似于局域网的串行链路,或者像Inmos Transputer上使用的专用互处理通信通道。超立方体使用松散耦合结构。由于松散耦合系统中的处理器都有自己的主存储器,因此没有任何理由需要使得两个独立主存储器中的任何内存位置匹配,因此除了为了避免处理器的输入/输出活动产生的不一致缓存副本而需要维护的步骤之外,不需要采取其他措施来保证缓存的一致性。
在紧密耦合系统中,确保缓存一致性的一个非常简单的方法是在内存中设置一个称为一致性域(coherency domain)的不可缓存空间。由于一致性域中的数据不会被缓存,因此该数据始终是最新的。这是一个在处理器之间进行仲裁的通信空间。一致性域方法被像Honeywell Series 66、早期版本的Intel i860和Elxsi 6400等系统使用。在某些微处理器中,也可以通过MMU页面描述符中的状态位来实现这种方法。使用这种方案的主要缺点是对程序员来说比较限制,因为所有的程序都必须在一致性域中处理不希望共享的任何数据或代码。不可避免地,专门用于一致性域的内存部分总是会发现对某些应用来说太小,而对其他应用来说则占用了过大的内存空间。当然,一致性域方法只能在软件与缓存架构同时定义的系统中使用,而不是缓存被设计用于加速现有的CPU/软件组合的性能。这种情况并不常见。
如果硬件和软件同时定义,还有其他更灵活的方法可供选择。其中之一是使用复杂的协议,通过定制编译器生成和存储与不同主存储器地址相对应的位,告诉缓存哪些主存储器空间是可缓存的,哪些是共享的。使用特殊编译器会使这些缓存无法在软件中透明地使用,因此它们不适用于代码源不可用的系统。不依赖编译器支持的协议称为朴素协议,在本章末尾的示例中将给出所有的例子。朴素协议在最广泛的应用中是比较有用的,但也比复杂协议更难理解。
4.2.1 Two Caches with Different Data
很明显,一个具有两个或多个带缓存的紧密耦合系统可能会遇到这样的情况:两个CPU的缓存都保存了相同的主存储器位置的副本。使用紧密耦合架构而不是松散耦合架构的主要原因之一是允许不同处理器之间共享任意的内存位置,如果这些共享位置不被允许进行缓存,它们将执行得更慢。

在4.1.1节中,我们讨论了确保DMA写周期更新其对应的缓存位置的问题,并探讨了实现一致性所使用的几种方式。在紧密耦合的多处理器系统中,必须采取类似的方法,以确保来自一个处理器的任何主存储器写周期更新其他处理器的适当缓存。一种已经有些不受青睐的方法是使用单个缓存来支持多个CPU,如图4.6所示。Univac 1100/82就使用了这种方法来支持两个处理器,Futurebus+也支持这样的配置。如果CPU可以彼此交错运行,这种方法可以实现;然而,如今和未来的CPU常常以如此高的速度运行,以至于没有离散的主缓存能够设计成足够快,以满足交错CPU的带宽需求。另一个问题是,使用相同缓存的双处理器出现抖动的可能性是单处理器使用相同缓存时的两倍。最后,在使用片上主缓存的系统中,有时会使用共享的二级缓存来减少总线流量。如果系统中的所有CPU都连接到同一个缓存,带宽限制就仅仅从CPU-主存储器接口转移到了缓存-CPU接口,而没有被减少。
4.2.2 Maintaining Coherency in Multiple logical Caches
在单处理器系统中,具有一致逻辑缓存的有很多限制,这些限制在4.1.2节中进行了概述。这些限制同样适用于多处理器系统中的逻辑缓存。其中最重要的是,缓存集地址位必须由内存映射方案的MMU页偏移位的子集组成,除非设计者想要采取一些非常极端的措施来保持一致性。
让我们以mips R4000为例,看看这些措施可以变得多么极端。请仔细阅读,因为R4000中使用的方案非常复杂。R4000是为多处理器系统设计的,使用了逻辑主缓存和物理次级缓存。次级缓存通过包含进行了主缓存的无效化筛选。主缓存的大小是主存储器页面大小的8倍,因此MMU中的三个页面号映射到了集位。这意味着主缓存中可能存在别名,必须进行次级缓存的监视。
解决这个问题的一种激进的方式是使任何可能与更新后的主存储器位置匹配的主缓存位置无效化。在次级监视命中时,所有与页面偏移地址位匹配的主缓存位置将被无效化。这种方法可以解决问题,但会通过引发不必要的无效化来降低缓存的性能。
mips解决这个问题的方式是将物理次级缓存作为系统的必要部分,并使用包含性,确保每个主缓存位置在次级缓存中都有副本。每个次级缓存位置都包含三个颜色位,指示次级缓存对应的主缓存位置。这三个位存储在次级缓存行的集地址中,就像标记位和行状态位一样。
当次级监视命中时,次级缓存将其三个颜色位与页面偏移位合并,以创建匹配的主缓存地址的逻辑集地址。然后,该逻辑集地址上的主缓存位置被无效化。在使用这种包含方式的系统中,同一主存储器位置的多个缓存副本不能存在,因为两个逻辑副本都会映射到相同的物理次级缓存地址,并且主缓存位置与其次级缓存副本之间有一对一的映射。如果没有这种一对一的映射,每个次级线路将需要存储多个共存的颜色位集,以应对所有可能的别名情况。
让我们看一下如果CPU试图将内存位置的别名放入其主缓存中会发生什么。假设存在主内存位置的主副本和次级缓存副本,并且处理器试图读取(并缓存)一个不同的逻辑地址,该地址将映射到相同的物理地址。这将被主缓存视为未命中/填充周期,而次级缓存则视为命中。颜色位将与三位较低的逻辑页位进行比较,并不匹配,因此次级缓存会将该周期视为类似未命中周期的处理,通过无效化具有匹配偏移和颜色位的主缓存位置来执行包含清理操作,然而次级缓存的副本不会被无效化或者逐出。次级缓存数据的新副本随后被复制到新的主缓存地址中,次级缓存行的唯一修改部分是颜色位,以反映该缓存行所代表的新逻辑地址。即使次级缓存的行包含已修改的数据,也不会进行主内存访问。正如我之前所说,这样做非常复杂,并且可能不值得投入这个努力。
4.2.3 Write-through Caches as a Solution
确保系统中两个或多个高速缓存互相协调的问题实际上归结为如何处理写周期的问题。DMA写入主存必须由各个高速缓存进行适配,而由另一个处理器执行的写操作不能与同一内存位置的另一个高速缓存中随后持有的数据产生冲突。确保这一点的一个非常直接的方法是使所有的写周期(无论来自DMA设备还是缓存处理器)都放在主内存总线上。写穿透高速缓存将它们的所有写操作发送到主内存,直接或通过写缓冲区,因此它们是这种系统的明显选择。这种方法是IBM 3033处理器以及Sequent Balance 8000系统中使用的方法。总线上的所有写周期都被所有高速缓存查询,如果写查询命中,所有具有匹配地址的高速缓存都将使其更新数据的副本失效或替换。正如之前提到的,某些系统在写查询命中时清空整个高速缓存。
在多处理器中,这种最后一种选择甚至比DMA系统中的选择更加不可取,因为其他处理器的写周期通常会很小、频繁且随机,而不是DMA的典型偶尔的大块数据传输。如果每个总线写操作都导致系统中的所有高速缓存都被清空,那么任何处理器的高速缓存所满足的周期数量将几乎为零。
一个问题围绕着写缓冲区的使用。假设一个处理器将某个地址的数据写入写缓冲区,而另一个处理器正在同时从主存读取相同地址的数据。当写缓冲区最终获得总线访问权限时,第二个处理器应该首先从主存获得现在过时的数据,并在使用它后立即使其无效或更新吗?在大多数设计中,这并不是一个真正的问题,因为处理器本来就不是同步的。然而,其他设计通过配置写缓冲区来查询总线(就像缓存一样)并要么中止其他处理器的读周期,直到写缓冲区被清空到主存,要么直接从写缓冲区向请求的处理器提供数据,从而缩短了一个处理器的写操作到另一个处理器的读操作的响应时间。第三种选择是在总线仲裁期间将写缓冲区赋予更高的优先级,而不是赋予任何读设备的优先级。这将始终允许写缓冲区在可能出现任何读冲突之前完成清空。
4.2.4 Bus Traffic Problems
既然我们从多处理器一致性问题的角度看到了写穿透高速缓存所提供的优势,现在让我们从总线流量的角度来看待相同的高速缓存。总线流量是一个真正的问题,通常是将高速缓存添加到任何系统中的原因,尤其是多处理器系统。自然地,总线流量越少越好,但设计者绝不希望系统的总线接近饱和状态。
首先,让我们假设所有总线周期都没有等待状态。这只是为了准确性而做出的暂时假设。稍后我们将回归现实。与以前一样,假设CPU周期中有10%的写周期,由于使用的高速缓存是写穿透的,所有这些写操作都会传播到主内存总线,既会被写入主内存,也会被系统中所有其他高速缓存进行嗅探。假设高速缓存的读未命中率为5%,由于读周期占CPU I/O周期的90%,因此读未命中将占所有CPU周期的4.5%。
写周期和读未命中将相当随机地发生。如果它们是完全可预测的,那么系统将迅速同步,在出现任何问题的迹象之前,高达六个处理器(100%/[10%+4.5%])将在这样的总线上尽可能快地运行。然而,现实世界中并非如此,每增加一个处理器都会导致仲裁和相关开销,因此在安装六个处理器之前,总线将导致新增处理器产生递减的回报。(请记住,我们基于一些假定的数字进行分析。这些数字极大程度上取决于缓存设计以及正在执行的软件。Sequent的Balance 8000据说能够在饱和之前维持多达12个CPU,而它使用的是写穿透高速缓存。)
现在,让我们看看当添加等待状态时会发生什么。假设这意味着每个总线周期都比前面的情况要长两倍,那么总线将变得饱和,并且只能支持一半数量的处理器。即使添加第二个处理器,递减的回报也很明显。
更让人不爽的是,现在考虑到所有这些主内存总线周期将争夺总线使用权,进一步堆积等待状态。大多数多处理器总线仲裁算法至少会增加一个等待状态,即使总线此时并没有使用。
我相信您正在看这个例子,并且说"通过增加读命中率和确保主内存系统尽可能快,有很多可以得到的收益",您是正确的,但是高速系统几乎不可能使用零等待总线设计,并且读命中率只能被推到一定程度。下一个选项是什么?由于写周期是总线上最常见的用户(在这个例子中为10%对4.5%),所以追求它们最有意义,对吧?
假设您的写穿透高速缓存,其未命中率为5%,可以转换为具有相似写未命中率的回写高速缓存。突然之间,总线仅需要处理所有CPU I/O周期的5%,而不是所有写操作加上5%的读操作,这看起来非常值得投入精力,但在多处理器设计中可能会引起许多困难问题。下一节将解决这些问题。
4.2.5 Copy-back Caches and Problems Unique to Copy-back Caches
我们现在已经决定,如果使用回写高速缓存将总线流量减少,那么在系统中总线流量的影响将大大降低,因此我们想知道在回写多处理器高速缓存设计中是否有任何特殊考虑因素会导致困难。确实有!
回写高速缓存的本质决定了它维护的数据可以与关联的主内存位置不一致。但是,高速缓存之间的缓存位置如何保持一致,并与主内存不一致呢?也许它并不总是必须一致!有可能,写入的位置仅属于单个处理器,例如循环计数器或私有指针。然而,在处理器之间共享的位置必须始终在各个高速缓存之间保持一致。尽管这似乎是需要编写支持协议的代码的地方,但实际情况并非如此,我们很快就会看到,确保共享的写周期适当地通信,而私有写操作则不需要,其实并不像看起来那么困难。
为了暂时从多处理器的复杂讨论中解脱出来,让我们看一下在具有回写高速缓存的单处理器系统中,用于确保DMA读周期使用最新数据副本的简单软件控制机制。与多处理器系统中遇到的问题相比,这个问题很简单,因为CPU启动了所有DMA活动,并且保证一致性的过程可以附加到允许DMA活动开始的例程上。在多处理器系统中,处理器之间的通信控制较少,可能会更加随机地发生。
为了支持单处理器系统中的DMA一致性,一些处理器指令集提供了一条指令,该指令启动所有脏(cache)位置的回写周期。这通常被称为缓存清除周期,因为该过程将所有脏位置从高速缓存中清除出去。如果控制从内存开始的DMA活动的程序(例如写入磁盘)通过执行清除指令来开始,那么当允许执行下一条指令时,将不会有脏位置存在。英特尔的Intel架构具有内部写穿透高速缓存,提供了一个写回使数据缓存失效(WBINVD)指令来支持外部回写高速缓存,但此指令只是引发一个标志,并期望外部高速缓存控制器阻止处理器执行进一步的I/O活动,直到所有脏的外部回写高速缓存行都已在主内存中得到更新。
缓存设计人员有时会对必须检查每个缓存行的状态并可能需要执行四倍或八倍于外部缓存中行数的写周期的指令的持续时间感到沮丧!更糟糕的是,一些处理器支持导出指令,该指令旨在将清除过程转化为一个软件循环,而不是硬件清除机制。在清除循环中使用导出指令将一个脏行的副本发送回主内存,以使该特定主内存位置在主内存中变得一致。当然,通过软件控制清除缓存对解决多处理器系统中备选处理器的随机嗅探读命中没有任何帮助,所以让我们继续向前看,面对更具挑战性的任务,通过硬件来确保任意一致性冲突。
在4.2.3节中提出的写透设计中,主内存被视为"真相"的持有者。任何需要数据的高速缓存都可以直接访问主内存,以获取最新的数据副本。在单处理器的回写高速缓存设计中,主内存地址的最新副本可能存储在主内存中或者缓存中,最新版本的位置由高速缓存行的Dirty位来指示。显然,在多处理器系统中,最新版本的位置变得更加模糊不清。

你可能立即想到的是,如果所有缓存的所有标签和Dirty位的副本都在一个集中的位置可用,那么每个进行读取周期的高速缓存都可以将其读取周期定向到主内存或者持有最新数据副本的高速缓存中。CDC computers和Chips and Technologies的M/PAX多处理器芯片组就是以这种方式操作的,这种方式有各种各样的名称:基于目录的、基于内存的、全局目录的或者内存标记。在基于目录的系统中(图4.7),每个与高速缓存行(有时称为块,有时不称为块)长度相同的内存位置或者内存位置组也会包含每个处理器的额外1或2个位。这些位指示该内存块作为一个或多个高速缓存中的缓存行的状态。该状态可能指示处理器A、B和F包含数据副本,并且尚未修改。在另一个块中,状态位可能指示只有处理器D有一个缓存副本,并且该副本是系统中唯一有效的副本。在一些更复杂的系统中,状态位可能指示D、E和F包含有效副本,但是主内存中的副本与这些副本不一致。所有这些状态位在系统初始化时都必须被重置。
无论如何,主内存充当裁判,必须不断了解每个处理器的高速缓存的状态。这在某种程度上简化了设计,因为所有的监控功能都由单个单元完成。另一方面,使用的处理器数量并不是任意的,而是受到分配给状态保持的主内存位数的限制。也许会有诱惑将这些位放在处理器板上,这样就可以随时添加处理器,但是扩展和减少主内存大小将需要同时修改所有处理器板。
这种方法并不像前文所示那么简单,因为必须选择某种方法,在一个处理器想要写入其高速缓存中缓存副本的内存位置时,不允许同时存在两个或多个相同主内存位置的副本。如果所有的高速缓存写入都必须先检查其他高速缓存中是否存在相同的行副本,那么主内存总线将被用于查询各个主内存行状态的周期而饱和。因此,许多协议不允许存在多个脏行的缓存副本。
一种更普遍的确保多个写回缓存一致性的方法是利用每个缓存内置的监听机制,这些系统被称为基于缓存的。可以采用几种方法来确保数据的一致性,并且在任何特定时刻,整个系统都不会存在多个副本,而这些副本都被认为是最新的。任何缓存行的状态都可以通过CPU周期或监听周期来更改。最常用的多处理器写回缓存一致性协议是所谓的写一次协议。有人将这些协议称为写优先协议。在写一次协议中,首次写入缓存中的位置也会同时写入主内存总线。换句话说,在第一次写入周期中,缓存行的处理方式就像写透缓存一样。为什么要这样做呢?这样做可以让其他处理器监听到写入周期,并使其无效化自己对相同行的副本。关键在于,它们应该使副本无效化而不是更新副本,因为这将是写入处理器允许进入总线的唯一写操作。写一次缓存的总线流量要比标准的写回缓存略高,但这是维护一致性所付出的代价之一。
一个处理器必须执行两种类型的写操作。第一种是对私有位置的写操作,正如我们所见,其他处理器不希望看到这些数据,比如循环计数器。第二种写操作是对公共数据或其他处理器希望看到的数据的写操作。在使用写一次协议的处理器中,私有数据的写操作遵循以下顺序:处理器首次将数据写入总线,并使同一内存位置的其他缓存副本失效,然后,在所有后续的写操作中,只在缓存中进行写入,以缓存速度而非主内存速度进行写入。所有这些后续写操作的总线流量也将被消除。只有在行填充期间腾出空间时,Dirty行才会被回写到主内存。从另一个角度来看,缓存控制器假设在第一次写操作周期中的数据是公共的,但如果能够在没有其他设备请求相同信息的情况下连续两次写入相同位置,那么控制器就确定该位置是私有的。
对于公共数据位置的写入周期将以相同的方式进行,直到另一个处理器请求该数据。如果只有第一次写入周期发生,其他处理器将从主内存中更新自己,并告知执行了第一次写操作的处理器,该次写操作不再是第一次写操作。需要执行另一个写一次周期才能使第二个缓存的匹配主内存位置数据的新副本失效。如果在另一个处理器请求副本之前已经发生了多次写入周期,那么主内存将不是最新的,请求的缓存需要获取第一个处理器正在写入的缓存行内容的副本。这种情况被称为干涉,因为具有最新数据的缓存必须干涉,强制读取设备无法看到主内存的当前内容,而是看到更新后的数据。干涉机制将很快进行探讨。

似乎每个缓存行需要另外一个状态位,告诉缓存控制器写入周期是第一次还是后续的写入周期。事实并非如此,因为典型的回写缓存具备支持未使用状态的所有机制。回顾第2.2.4节,典型的回写缓存使用了两个状态位:有效位(Valid)和脏位(Dirty),但通常这些位仅表示三个状态:无效(干净或脏)、有效(干净)和有效(脏)(图4.8)。
如果我们对这两个位进行更详细的解释,允许我们使用未使用状态更好地跟踪缓存行的状态,会怎样呢?这将允许我们存储四个状态:无效、从未被任何CPU写入过的有效、被该CPU写入一次的有效以及被该CPU写入多次的有效(缓存设计者不使用这种术语,但是有更简洁的命名方式,我们将在稍后的第4.3节进行讨论)。通过这样做,我们不能再称这些位为有效和脏位,因为它们的含义现在已经编码,但是我们仍然拥有相同数量的位,并且对于支持这种新协议的缓存控制器所需的改变是简单的。唯一需要缓存控制器进行干涉的状态是缓存中的数据比主内存中的数据更加最新的状态。这就是写入多次状态,有时也被称为私有状态,因为它是缓存中的数据与主内存或其他缓存中的数据不共享或不一致的唯一状态。
一个稍微变化了写一次算法的协议被称为广播或写广播。与写一次方案中由第一次写入引起的使其他缓存副本失效不同,广播缓存协议允许接收新写入数据的缓存保留其缓存中由其他CPU/缓存组合通过总线写入的数据的副本。因此,广播缓存必须通过系统总线周期来区分广播写入和旨在使其他缓存副本失效的写入操作。此外,还需要一整套新的状态来适应这种新方法。稍后的章节将介绍一些广播协议。
干涉有两种类型:间接数据干涉和直接数据干涉。两者都值得深入解释。我们先从更简单的间接数据干涉开始说明。
如先前所述,当缓存检测到对一个被标记为"脏"的位置的读取探查命中时,就会出现需要解决的问题。间接干涉缓存将终止探测读取周期,接管主内存,从被探测地址向适当的主内存位置写入数据(不需要CPU参与),将该行状态从脏状态更新为干净的有效状态,然后释放总线,以便其他CPU可以再次尝试读取所需数据并成功地完成操作。(在探测周期期间更新主内存的过程有时称为XI castout,其中XI代表"交叉询问强制退出"(cross-interrogate castout)。"交叉询问"就是我们已经看到的另一种探测方式,"强制退出"实际上是驱逐,但是术语XI castout的创建者可能并不是真的指驱逐;他们可能执行更新主内存和修改行状态到"有效且从未被任何CPU写入过"的状态,就像你和我一样。)
这种间接干涉之所以称为间接,是因为探测处理器缓存中请求数据的请求不是通过探测的缓存直接将数据交给请求设备,而是通过主内存传递数据,这是一种更为间接的路径。这让人想起一些老电影里的对话,三个人在一个房间里,其中一个告诉另一个告诉第三个某些事情,因为他俩不愿意直接说话。一些间接数据干涉协议允许一个已经被终止请求的设备观察总线流量,如果发生了对匹配地址的写周期,则获取一份副本并继续进行读周期操作,省时并减少了总线流量,相比需要重试读周期的协议。
间接数据干涉确实需要特殊的总线支持。根据总线协议是否支持分裂事务,使用两种中的任何一种方法来终止总线周期,必须提供一种机制,以允许从探测位置的主内存更新优先于其他处理器的任何周期,即使另一个处理器通常可以获得更高的总线权限。
在分裂事务系统中(首次定义于2.1.2节),总线主控发出请求并将自身从总线上移除,等待任务执行者的响应。在分裂事务系统中,被监视的缓存向请求设备发送一个终止/重试的消息,然后在发出另一个请求之前获取对总线的控制权。
在没有分裂事务的系统中(其中总线主控发出请求并一直保持在总线上直到收到响应),要么一个优先级更高的请求强制将请求处理器从总线上移除并将其置于保持状态,直到主内存更新完成,要么一个终止/重试信号导致周期重新启动,但在此期间,一个优先级更高的主控,即被监视的处理器,获得对总线的控制权以更新主内存。显然,如果读周期来自一个在争取总线方面具有更高优先级的处理器,且对探测周期的响应不允许暂时提高优先级,则系统将被锁定,因为更高优先级的处理器在等待较低优先级的处理器无法将数据放入主内存时。
直接数据干预系统允许任何响应的缓存在总线读取探测周期上禁用系统内存,并将探测位置的数据放置在总线上,从而实现了探测的缓存与请求处理器之间的即时通信。这也被称为缓存对缓存传输,因为主内存不参与交易。在这些系统中,可以选择两种方法来解决多个缓存副本存在且主内存没有有效副本的数据问题。最简单的理解方法是使用反射,即主内存具有类似于系统其他CPU缓存上的缓存探测机制的探测机制。其他处理器上的探测机制通常使用缓存标记RAM来监视总线上的匹配地址周期。另一方面,主内存由一块单一的大连续地址块组成,所以它只需要一个地址解码器,实际上是与用于分配主内存的存储空间相同的地址解码器,如果没有被介入的缓存取代,通常会映射到主内存。
如果处理器开始对其它处理器的缓存中的一个地址进行读取周期,并且该地址在另一个处理器的缓存中存储为"多次写入",则被监视的处理器禁用来自主内存的响应,并将自己的数据放置在总线上,将其状态降级为"有效但未写入"。内存的探测机制检测到了一个未被允许支持的读周期,因此它抓取了在总线上出现的数据副本,并将其暂时放置在主内存系统的写缓冲区中。随着内存延迟的允许,新数据被写入到主内存中。最终,所有缓存和主内存都拥有相同的数据副本,因此该行的所有缓存副本的适当状态为"有效但未写入"。
直接数据干预系统不使用反射的一种缓存状态设计,旨在允许多个缓存副本存在于未在主内存中更新的数据中。这些状态被称为所有权协议。再次假设一个处理器的缓存包含另一个处理器正在请求的主存储器位置的脏副本。当包含更新副本的缓存监视此读取周期时,它将向请求处理器提供数据,就像前面的例子中一样,但主内存不会配备任何在事务发生时更新自身的手段。事实上,主内存是一种传统设计,只有在系统总线写入周期时才会对其进行修改。在这种缓存中主内存更新的唯一时机是当脏行从缓存中逐出时。这需要一组不同的状态,因为现在可以在两个不同的缓存中存在相同位置的两个脏副本。这就是所有权的概念发挥作用的地方。在所有权协议中,修改脏缓存位置的CPU所在的缓存被认为是该位置的所有者。只有所有者负责最终更新主内存,而其他缓存则不负责。因此,当属于拥有该行的缓存的脏行被替换时,该脏行将从缓存中逐出并在主内存中更新,而在不拥有该行的缓存中替换相同脏行将不会导致主内存更新。那么如果没有缓存拥有主内存位置的副本怎么办?这并没有被广泛讨论,但大约一半的论文认为主内存拥有任何潜在未被任何缓存声明的缓存行的所有权,而另外一半则说该行是无主的。主存储器对一行的所有权平衡了东西,并且似乎更容易描述所有权统一性缓存协议中采取的操作,因此我们将在本书的其余部分使用该命名法。当这种协议的缓存需要更新一行时,它将请求所有者提供该行,不管所有者实际上是否是主存储器。
使用所有权协议的缓存解决的一个问题是pingpong的困难,这有点像抖动,因为一行的状态经常改变,导致该行的时序和总线流量增加。除了拼写和发音都很难之外,"pingpong"是一种现象,即正在被缓存审查但不拥有该行的缓存将该行置于私有状态。在间接数据干预系统中,总线通常被占据,因为读取缓存执行了私有位置的无效读取,响应缓存将通过内存写入周期回答,随后让读取设备重新进入总线。然后,正在被修改的副本将再次变为私有状态,以广播写入的代价,只是以便请求缓存进一步审查。这样做不需要太多就能显著地占用主内存总线。作为所有权协议的一个例子,我们将描述一种使用四个状态的典型直接数据干预复制回缓存,这些状态与先前在间接数据干预示例中使用的状态有些类似:无效、有效、CPU仅写入了一次的有效和已经由CPU写入了多次的有效。你会说:"嘿!这几乎与我们之前讨论的协议相同。" 好吧,或者也不相同。请耐心等待,您将看到直接数据干预为协议带来的差异。
最后两种状态是声明该缓存是真正拥有副本的所有者。如果我们观察一个给定行通过其各种状态变化的过程,我们会看到类似于以前的例子中的进展,直到遇到嗅探读命中为止。假设处理器A和B都从无效行开始,并且对于相同的主存储器位置都得到了读取失误。这些行将被更新,并且它们的状态将从无效变为有效。然后处理器A可能会写入这条线路,当其缓存控制器看到状态从有效到仅由该CPU写入一次的有效状态转换时,将广播写入以使总线上的其他副本无效。让我们再让处理器A写入该行,将行的状态从仅被此CPU写入一次的有效状态提升到被此CPU写入多次的有效状态。处理器B缓存中的副本被第一次写入使其无效,由处理器A的缓存广播到总线上,因此在下一个针对此地址的读取失误时,处理器再次尝试更新该行。现在,这个尝试由处理器A的缓存中介完成,行的状态存储在处理器B的缓存中,就像来自主存储器一样。如果处理器B然后替换了这条线路,即使处理器B的缓存中的副本比主存储器中包含的副本更新,也不会发生主存储器写入周期。如果处理器A驱逐该行,其缓存将更新主存储器,并且处理器B的缓存将窥探此更新,但不需要更改该行的状态。该行的所有权已从处理器A的缓存放弃回到主存储器。经过仔细检查,读者会注意到,在这个例子中拥有处理器(处理器A)无法知道其他处理器的缓存中是否存在其被多次写入的有效副本。为了与这些副本保持一致性,拥有处理器必须将每个写入周期放在总线上,从而消除使用复制回退协议的任何优势。需要做一些工作,让缓存知道是否必须广播写周期还是可以保持私有。这要求将被此CPU写入的有效状态进一步划分为两个类别:嗅探和未嗅探的。未嗅探的副本将执行立即的缓存写入周期,而不进行任何总线交互。嗅探副本则知道另一个处理器可能有脏位置的副本,因此需要执行总线写入周期来使其他副本无效;然后拥有缓存就可以将该行的状态重新变为未嗅探。这就是所有权的第五个状态出现的地方。我们将将被此CPU写入的有效状态重命名为被此CPU写入但未经嗅探的有效状态,并将第五种状态设置为被此CPU写入并经过嗅探的有效状态。像这样的缓存通常仅适用于某些总线协议。目前最常见(也可能是我们讨论的最慢版本)的是具有串联层次结构的总线,其中只有在所有其他设备都有机会替代它后,主存储器才会响应。如果有几个缓存连接到总线上,那么在允许主存储器最终满足读取周期之前,每个缓存都有机会响应。分离事务总线是列表中的下一个,就像它们在间接数据干预系统中的使用一样,在主存储器访问延迟之前,它们允许响应缓存给自己留出时间段来响应,向主存储器发出信号说明该周期已由另一个响应方满足。第三种方法涉及使用辅助总线,其提供嗅探数据,而主存储器总线专门用于主存储器传输。然后,读取哪个总线的决定是由请求CPU /缓存的处理器板作出的,而不是系统功能。要支持两总线协议以重新路由读周期,需要不同一组行状态,具体取决于读取处理器希望找到所需的线路的位置。使用在第4.3.4节中讨论的N+1协议的另一种方法是使用智能主存储器。智能主存储器为每个潜在缓存行保留一位,指示该行是否由主存储器拥有或不拥有。当我告诉你这些系统的设计者将不保存此类状态的主存储器称为愚笨的主存储器时,你不会感到惊讶。智能协议是基于缓存和基于存储器的协议之间的混合体,具有更倾向于基于缓存的一侧。
一次写入和所有权协议有时利用写入分配作为一种方法来表明需要其他缓存使任何现有的缓存行失效。这意味着主内存的唯一写入周期将是非可缓存地址(如果存在)或复制回路,这两个周期都不会导致失效,因为没有其他缓存包含这些行的副本。支持此类协议的总线需要机制,在snoop读命中而不是snoop写命中期间允许行失效。随着我们详细研究此类协议,将更清楚地了解到总线将需要支持两种完全不同的读取周期,其中一种允许其他缓存维护所请求行的现有副本,而另一种需要使匹配的行失效。一些示例是Read-for-Ownership或私有读取、Read Shared或公共读取、Write-for-Invalidate和Write-without-Invalidation,它们在第4.3节中有描述。虽然这些状态与维护缓存一致性密切相关,但我们将把它们视为总线协议问题,只描述足以允许彻底理解缓存操作而不是总线操作的内容。但是,总的原则是,Read-for-Ownership周期仅用于写入分配周期,而不用于满足读取未命中。然而,在复杂的所有权协议中,编译器会指示CPU指示读取未命中将在稍后导致写入周期,因此CPU会在编译器决定最好立即使所有其他缓存中的位置无效的位置上对读取未命中进行Read-for-Ownership。尽管大多数设计不能使所有程序都编译以适应缓存的体系结构,但它们依靠幼稚所有权协议的写入未命中来生成其Read-for-Ownership周期。

一个真正不同寻常的所有权一致性协议的好处是,拥有所请求数据副本的缓存可能响应速度比主内存更快,导致某些硬件和软件组合表现出比系统中包含的处理器数量更大的性能增益(图4.9)。这是怎么回事呢?随着越来越多的处理器被添加,存在更多的快速缓存位置可以向其他处理器提供数据,比主内存更快,甚至在一个足够大的系统中,只要有正确的程序,它们就只会将主内存用作放置驱逐线的地方或可能作为DMA的分配区域。当然,这种现象的好处高度依赖于由基准测试软件支持的处理器间通信的数量。有些多处理器系统充分利用这个概念,有时被称为缓存自己的一部分主内存。这是一个缓存和主内存之间分界线变得不那么清晰的地方,一些设计师会称之为缓存,而其他人则认为它根本不是缓存。 Kendall Square Research Corporation就是这样一家公司,他们采用的方法被称为Allcache,意思是将整个主内存实现在几个CPU的缓存中(从16到超过1000个)。在一个非统一内存体系结构(NUMA)中,各种主内存部分被物理上分配在系统中的不同处理器板之间,本地CPU卡所对应的主内存部分对本地CPU的响应速度比其他处理器要快得多,不仅因为接近,还因为本地处理器不需要仲裁系统总线,并且始终优先访问本地内存。从任何处理器的角度来看,主内存将会有一小段快速地址范围,但其余的地址范围都很慢。获得最佳NUMA机器性能的方法是编译代码,尽可能多地引用本地和快速的主内存部分。这意味着代码必须设计以适应硬件,这常常不是可用于系统设计团队的选项。当然,NUMA机器的反面是统一内存体系结构(UMA)机器,其中主内存对所有处理器都是平等可访问的,并且任何地址都会以相同的延迟响应所有处理器。
在本节中,还应该提到支持多个写回缓存的总线另一个命令:后退命令。在某些缓存设计中,后退只是一种总线命令,要求缓存立即停止与系统总线的交互。在复制回缓存一致性协议中,含义是不同的,存在两种类型的后退。完全后退是指当取走命中不得到服务,直到被取走的缓存能够在自己方便的时候服务为止。另一种是受限制的后退,这是指取走命中会导致所有CPU /缓存交互立即停止,以便可以立即服务取走命中。据我所知,没有大量关于哪种优于另一种的思考或研究;它们只被视为备选的窥探技术。
4.3 EXISTING COPY-BACK COHERENCY PROTOCOLS
本节将展示一些现有的多个写回缓存一致性协议的真实例子。你可能已经感觉到这一切变得非常复杂,为了帮助你,我们将通过表格的形式进行说明,对每个协议进行详细说明。这些表格可以很好地定义缓存的状态,所以如果你设计一个缓存,尝试自己制作一个表格会很有帮助。表格中的列表示在任何周期开始时缓存的状态。每一行表示可能对缓存操作或状态改变产生影响的外部事件。本书中的表格并不足以用于设计,你可能会发现自己需要组合几层类似的表格来描述自己硬件的各个部分的动作。
4.3.1 The MESI Protocol
MESI是一种基于写一次缓存的协议,可能是在行业杂志中描述最频繁的一种。为什么呢?因为它的名字很有吸引力。当我告诉妻子(她是神学专业的学生,不是工程专业)一个计算机可以使用MESI协议,使用SCSI通信,并提供图形用户界面时,她觉得很有趣。然后,这与所有这些肮脏的语言似乎不一致,MESI协议排除了缓存使用Dirty位的可能性。
MESI这个首字母缩写代表了一种典型的间接数据干预协议的四个状态,我们在4.2.5节中进行了描述,但为这些状态使用了其他较少描述性但更简单的名称:Modified、Exclusive、Shared和Invalid。下面是这四个状态如何与4.2.5节中所描述的状态对应:Invalid自然而然地对应Invalid状态;Shared是我们最初称之为Valid-and-never-written-to-by-any-CPU状态的状态;Exclusive是指此CPU的Valid-and-written-to-once状态;Modified是MESI协议用来表示Valid-and-written-to-more-than-once-by-this-CPU状态的名称。正如我们将在下一节中看到的,这些状态不仅适用于间接数据干预。
这些命名非常有意义。Shared状态是唯一允许将同一内存位置的另一个副本存储在其他缓存中的状态。如果一个缓存具有Exclusive或Modified行,其他缓存中的所有匹配行将被标记为Invalid(我们将在表4.1中详细介绍这一点)。Exclusive状态告诉缓存控制器,主内存位置与缓存的内容保持一致,并且没有其他相同主内存位置的缓存副本存在;这是主内存位置的一份独家缓存副本(有些人将Shared状态称为非独占状态)。最后,Modified状态表示该地址的唯一当前版本位于此缓存中。
MESI协议最初是为多总线的原始版本开发的,这种总线没有任何支持一致性协议的钩子。因此,它没有写分配或直接数据干预。在简单总线上实现MESI系统时,所有写周期都被视为写入使其无效,并自动使其他缓存中匹配的位置失效。这在表4.1中显示出来。

由于此类系统中的总线不会支持读取使其无效状态,因此在设计MESI缓存时不使用写分配。表4.1显示了在可缓存空间中处理处理器和嗅探器的读取和写入命中和未命中时,四种MESI状态和对应的响应。大多数CPU周期的执行方式与写透或复制回缓存中的类似周期相同,具体取决于事务发生前缓存行所处的状态。两种特殊情况是对已修改行的写未命中和对标记为Exclusive的行的写命中。对已修改行的写未命中可以引起主内存写入周期,而不更新缓存,或者如表4.1所示,可以驱逐当前行的内容,然后进行写透周期,将该行转为Exclusive状态。对Exclusive行的写命中将把该行更新为Modified状态,而不需要总线事务。必须先进入Exclusive状态,然后才能进入Modified状态。不允许缓存行直接从其他两种状态直接进入Modified状态。
总是有特例存在。英特尔的处理器使用写分配实现MESI协议,以弥合处理器的16字节内部缓存行大小和处理器支持的1到4字节写周期之间的差异。这意味着所有写未命中将在处理器总线上显示为读未命中。在这种情况下,如果系统首先将所有读未命中假定为写分配未命中,并且新获取的缓存行的状态自动设置为Exclusive,同时将所有其他缓存副本自动失效,可以提高性能。这要求缓存/CPU组合要获得支持具有读取使其无效机制的主存储器总线,以便读周期可以引起其他缓存的失效,除非设计者愿意忍受偶尔的pingponging问题,即共享一块数据的两个缓存不断相互使对方的副本失效。
在MESI协议中,嗅探周期的处理方式与你对复制回缓存的期望基本相同。忽略嗅探读和写未命中,只有当嗅探的缓存行处于以下状态时才真正注意到嗅探读命中:1)处于Exclusive状态,这种情况下,独占性将降级为Shared状态(主存储器中已存在有效副本),或者2)处于Modified状态,需要中止其他设备的读请求,并允许嗅探缓存将Dirty行的内容更新到主存储器,然后允许嗅探处理器重试中止的总线周期。无论哪种情况,该行的状态都将更改为Shared状态。所有写嗅探命中将使命中的缓存行失效,但对于已修改行的写命中通常意味着软件管理上出现了一些严重问题!
真正的极简主义者会想知道是否可以完全取消Exclusive状态,确实可以,但代价很大。缓存控制器可以轻松确定缓存是从Shared状态向Modified状态转换,并发出总线写入以使任何其他缓存副本失效,就像从Shared状态转换到Exclusive状态一样,但如果该行再也不被写入,缓存控制器就无法得知,并且会驱逐Modified行,无论它是否与主存储器的副本保持一致。这与减少总线流量的目标不符,因此很少使用,但是此方法的一个版本将在第4.3.4节中讨论。
4.3.2 Futurebus+
一个开放的标准总线协议,IEEE Futurebus+(加号让我想起小写字母t,但我怀疑这并不表示该总线会受到有限的接受),利用了总线专家自其他更好的已建立的开放标准总线首次设计以来所积累的丰富理解和发展。Futurebus+使用一种称为总线转换器逻辑(BTL)的具有自己电压I/O级别的分时同步总线,尽管它们与标准逻辑电平如ECL和TTL不兼容,但通过极快的速度弥补了任何差异。Futurebus+基于MESI规定了一种复制回写一致性协议,但假定比原始MESI模型拥有更多的总线支持,使用写分配,并包括了一个被称为"snarf"或"write snarf"的附加机制。当处理器尝试将数据读入缓存,但总线不可用时,缓存能够监听其他正在进行的事务,并且如果请求的地址恰好涉及总线事务,那个处理器的缓存控制器将获取一份副本,并停止尝试争夺总线的控制权。Futurebus+规范允许设计者从广泛的选项菜单中进行选择,并提供了一个选项,允许缓存复制与无效位置地址匹配的总线事务的副本,即使处理器并没有请求它的副本。这看起来很像反射,不是吗?在Futurebus+委员会决定模拟主存储器的反射技术时,几乎是禅意的,因此缓存现在从缓存到主存储器的传输中捕获数据,就像反射式内存在缓存到缓存的传输中捕获数据一样。将反射技术应用于处理器和内存卡上也非常有意义。一些读者可能持有不同意见,那肯定是因为你已经考虑到在处理器尝试获得总线控制权时,处理器所请求的确切地址在总线上的几率几乎为零,并且缓存应该几乎完全填满有效位置。反驳有两个方面。首先,任何可以减少总线流量的协议都将进一步增加可以连接到总线上的处理器数量,并且随着总线流量的减少,此类交互事件发生的可能性将成为整体事件链中更大的一部分。其次,在处理器运行足够长时间以填充其缓存后,被标记为无效的缓存位置很可能曾经是有效的,然后由于另一个处理器将该行置为独占状态而使其失效。如果地址再次出现在总线上,这就自动意味着该行正在从独占状态降级,并且可以在系统中的其他缓存中再次复制。仅仅因为使用Futurebus+,并不要求在每个处理器板上都有复制回写缓存,甚至不要求有多个处理器,但由于总线包含了支持最广泛实现的所有协议,因此可以轻松地实现混搭系统,包括写直通、复制回写和非缓存处理器板。正如我之前所说,有各种方式来实现您自己的板卡,并确保兼容性。


Futurebus+与其基于的MESI协议不同之处在于,它使用直接数据干预(从其他缓存而不是主存储器中读取可缓存位置)和写分配,并且支持了一种围绕缓存实现的分时事务总线协议,而不是反过来。它有三种读周期、两种写入和一种使其他缓存中某个缓存行副本失效的无效命令。相比于Read-for-Invalidate或Write-for-Invalidate命令,这种无效命令更快速地使其他缓存中的缓存行副本失效。此外,通过一种名为tfI'的信号,Snoop命中确认从被侦听的缓存广播到总线上。tfI'是一个总线信号,其定义取决于总线上正在发生的事务类型。在本节中的所有周期中,tfI'表示Snoop命中。通过监视tfI'信号,请求的缓存可以选择立即将一行线路置于排他状态,如果该行线路在其他任何缓存中没有复制。这使得表4.2比MESI的表4.1更复杂,但这样做是值得的,因为它可以减少不必要的写入主存储器总线的次数。内存子系统(如果存在)也需要反射操作,这是另一种节省带宽的措施,因为可以使用高速直接数据干预周期而无需后续周期来更新主存储器。
等等!关于内存子系统的"如果存在",这是什么意思?Futurebus+的定义允许整个系统在没有主存储器的情况下存在,只要任何活动的主存储器地址在系统中始终得到记录。为此,有一个术语称为"最后机会库",即该地址最终停留的位置。在具有主存储器的系统中,这是该行的所有者,通常是主存储器。在仅缓存的系统中,最后机会库是行的当前所有者,并且通过总线协议的其他部分(我们在这里不讨论)确保该行始终在系统中的某个位置复制。
要理解使用表4.2的Futurebus+总线命令,应该定义四个状态的名称。它们是排他修改(对应于MESI协议中的修改状态)、排他非修改(MESI的排他状态)、共享非修改(共享状态)和无效。最容易理解的两个总线命令是用于I/O总线主设备(通常是DMA设备)的Read Invalid和Write Invalid命令。尽管Write Invalid命令会自动使所有现有的缓存副本失效,但Read Invalid命令并不会这样做,而是将副本降级为共享非修改状态,并导致排他修改副本的所有者在发生Snoop读命中时进行干预。
Read Shared命令由经历读取缺失周期的处理器发出,任何散播命中都允许数据来自主存(如果散播的副本是共享未修改或排他未修改)或来自散播高速缓存,如果散播命中是指向独占修改位置。在所有这些情况下,散播的高速缓存将断言tP并且会停留在共享未修改状态。 (作为灵活性,Futurebus+规格允许高速缓存无效其行的副本,如果它不想断言tP)。由于主存中使用反射,从排他修改到共享 downgrade 不会导致一致性问题。无论何种情况,在任何Read Shared周期结束时,线路的一个有效副本保留在主存中。现在请求的高速缓存知道是否存在其他缓存副本,因为如果存在任何其他缓存副本,它将收到已断言的tP,如果没有,则收到否定的tP。在这种情况下,如果存在其他缓存副本,新的高速缓存行将被加载为共享未修改状态,如果没有其他缓存有副本,则将立即将其加载到排他未修改状态。如果加载为排他未修改的行随后成为高速缓存写命中的主题,则无需调用写-先操作,从而节省总线带宽,超过标准的MESI实现。最后一个Futurebus+读取周期是读取修改,它由高速缓存使用以信号它遭受写入缺失周期,并且必须为其写入配置申请独占副本。当Read Modified snoop命中在高速缓存上发生时,该高速缓存的行被无效。由于该行可能比正在写入的字长(Futurebus+的行长度为64字节)更长,并且由于散播的排他修改副本可能会与请求处理器将要修改的不同字节进行修改,因此,散播的排他修改行的所有者必须执行直接数据干预以将数据交给新的所有者。两个Futurebus+写周期是Write Invalidate,它已经讨论过了,以及Copyback。Copyback周期用于排除排他修改行。由于这些行是独占的,因此其他任何高速缓存中都不存在副本,因此永远不会有任何散播命中。唯一涉及高速缓存交互的其他Futurebus+总线周期是无效周期。无效是由拥有共享行副本的高速缓存发出的,它想将该行转换为独占修改状态。奇怪的是,Futurebus+委员会决定将其称为写入缺失周期。为了保持一致性,我将遵循更普遍的惯例,并将其称为对共享未修改位置的写命中。在散播无效命令时,任何拥有共享未修改行副本的其他高速缓存都将其副本无效。其他高速缓存中的副本不可能具有任何其他状态,因为在一个高速缓存中处于共享未修改状态的行不能处于任何其他高速缓存的两个排他状态中的任何一个。采用这种相对较落后的方法,我们讨论了表4.2底部的所有散播周期,并跳过了CPU周期。让我们回顾一下。在CPU读取缺失时,如果要被替换的行处于独占修改状态,则必须在读入新数据到高速缓存之前将其从高速缓存写入主存,使用Copyback总线命令来完成。然后,与在任何其他状态的行上发生的CPU读取缺失一样,放置了Read Shared命令在总线上,数据的所有者作出响应,任何散播高速缓存都有机会断言tf*,告诉请求的高速缓存该行确实是共享的。该行复制到高速缓存中,如果断言tf*,则该行标记为共享未修改,但是如果没有断言tf*,则该行立即转换为排他未修改状态。当然,请求的高速缓存不必绝对将该行放入排他未修改状态,因为Futurebus+允许这种选择,但这是一种肯定可以增加整个系统性能速度的选择,因为它消除了执行随后的一次写-一次周期的需要。当读命中时,数据直接从高速缓存传输到CPU,没有任何总线交互或状态更改。
在写命中时,独占修改行将被写入而无需总线交互或状态更改,而独占未修改行将被写入而无需总线交互,但会经历状态升级为独占修改。如果该行最初处于共享未修改状态,则其他副本必须被作废,因此缓存控制器必须在总线上发出作废命令。这是一个快速的、缩略版的写周期,其中数据从未放在总线上。然后可以写入缓存行,其状态将升级为独占修改状态。
写失效周期与读失效一样,在清空任何可能的脏行后都会以相同方式满足。驱逐处理方式与读失效相同:如果该行处于独占修改状态,则首先使用复制回总线命令将其写入主存储器。然后,对于任何行状态,要写入的行都将使用读修改命令从其所有者读取到缓存中,作废所有其他缓存副本,写入到缓存中并立即进入独占修改状态。忽略tf*的状态,因为读修改命令将使任何被监视的副本无效。Futurebus+的另一个独特特点是,它允许在分层总线结构(如图4.10所示)上维护一致性。解决这个令人费解的问题的关键是使用两种代理:内存代理和缓存代理。内存代理在总线的一侧接收读写命令,并响应这些命令,使其在桥的另一侧看起来是从一个简单的主存储器出现的一样。在另一侧,内存代理跟踪缓存和主存储器位置,监视每行的所有权,并逐个基础与每行的所有者进行交互。由于内存代理必须知道何时去主内存还是缓存获取数据的当前值,因此在桥的内部有缓存的内存代理不能使用独占未修改状态。独占未修改状态允许行进入独占修改状态而无需总线交互,因此内存代理无法发现所有权从该总线上的内存转移到缓存时的情况。如果再次查看表4.2,您可以看到仅在不激活tf*线的情况下服务于读失效时,才会进入独占未修改状态,而且只有设计师希望实现这一点。否则,读失效将将新行带入共享未修改状态。如果缓存驻留在由内存代理监视的桥的一侧,则对于内存读失效中的tf*的响应被简单地禁止。

缓存代理只需要关心缓存,因此它较为简单。基于包含性,缓存代理从它所代表的缓存使用的一侧桥上的总线进行嗅探。缓存代理知道其自己总线上缓存的每条线的地址,并仅允许通过与其保护的缓存中可能导致嗅探命中的那些嗅探周期通过到桥的另一侧。这是一种削减桥上流动的总线流量的方法。通过使用缓存代理和内存代理,可以在图4.10的分层结构中沿着层次结构传播嗅探,而不会将所有总线绑定在大量无用的嗅探流量中。
另一个新的多处理器总线规范是Corollary C-bus II。该规范非常类似于Futurebus+,因为它使用了改进版本的MESI协议,并支持写分配、32字节行和直接数据干预。为了支持所有这些,提供了两种特殊的读独占总线周期:读独占和清空独占。两者都用于支持写分配,清空独占命令在写入分配读周期之前使用脏行进行清空。通过使用这些周期从嗅探命中的副本中读入缓存的线将使所有其他缓存中的这些命令作废。
4.3.3 The MOESI Protocol
尽管MOESI(Modified, Owned, Exclusive, Shared, Invalid)的首字母缩写看起来与MESI几乎相同,但实际上它代表了一个非常不同的协议,因为它支持直接数据干预。MOESI的首字母缩写代表的状态与MESI中的使用的名称相同,只是增加了Owned状态,但状态的含义大多不同,并且与第4.2.5节中使用的长名称状态相对应,具体如下:Invalid仍然是Invalid状态;Shared也是最初被称为Valid的状态;Exclusive转变为Valid-and-written-to-once-by-this-CPU;Modified现在成为Valid-and-written-to-more-than-once-by-this-CPU-but-unsnooped;而Owned则表示Valid-and-written-to-more-than-once-by-this-CPU-and-snooped。Modified、Owned和Shared状态显示了MESI和MOESI之间的区别。在MESI中,如果一个处理器有一个Shared line,这意味着主存储器是当前的,并且该行在任何缓存中都没有被写入。在MOESI协议中,Shared表示这只是一个位置的副本,可能是当前的主存储器,但是是所有者数据的精确副本。在MESI中,Modified状态隐含地意味着该位置没有被监视,而在MOESI中,需要明确说明这一点。最后,Owned状态解释了MOESI的直接数据干预和MESI支持的间接数据干预之间的区别。Owned位置可能在其他缓存中有副本,类似于Shared line,但主存储器从未被更新,因此在驱逐时必须由拥有缓存进行写入。

表4.3显示了五个MOESI状态,并列出了在可缓存空间中处理器和嗅探读写命中和失效时的响应。与MESI一样,MOESI模型假设没有特殊的总线支持。状态转换在下面的段落中详细描述。
在CPU读失效时,如果目标行是Owned或Modified状态,它将被驱逐。然后,缓存会从主存储器或拥有缓存中更新该行,并将其装入为Shared状态。如果行来自拥有缓存,这意味着此缓存副本处于Owned或Modified状态,如果状态为Modified,则会降级为Owned。
在对Shared和Exclusive行进行嗅探读命中时,不会进行总线交互,但如果Exclusive副本收到嗅探命中,则会将其自身状态降级为Shared。
与MESI协议类似,所有主内存写入周期都会被其他缓存进行检查以进行失效操作。MESI和MOESI设计通常不使用写分配,这意味着CPU的写失效必须通过总线写入周期在系统中进行广播。然后,写入周期将类似于写透缓存一样通过缓存进行传输。在许多设计中,缓存会忽略写失效。忽略写失效的假设是,当写入时,如果缓存中没有该数据,则很可能在未来也不会引用它。对于加载到各个区域的初始化值等项目确实如此,但对于第一次设置内存中的循环计数器或堆栈达到某个特定深度时并非如此。然而,这些是例外而不是规则,因此MOESI缓存通常会忽略写失效。
在表4.3的示例中,我们选择了更新丢失行的较复杂方法。只有当缓存行与系统的CPU/软件支持的最小写入周期大小相同时才能这样做(并非所有软件在CPU提供时都使用字节写入周期)。如果允许较小的写入,并且要将写失效写入缓存,应考虑写分配协议。该表假设缓存仅支持字写入。由于该设计要求在写失效时替换修改行,因此总线随后必须进行两个周期,即逐出和写入(用于失效)替换行。
如果采用写分配,那么在五状态MOESI之上,以下几个三态和四态协议中的一个可能会被选中。在具有写分配的MOESI系统中,对修改的位置的写失效将消耗三个总线周期:首先是修改行的逐出,然后是读取周期以允许分配,然后是一次性写入周期以使其他缓存中的任何副本失效。这可能是一个相当大的惩罚!
在CPU写命中时,如果位置处于修改状态,则没有总线活动,行将更新并保持在修改状态。类似地,对标记为独占的行的CPU写命中将更新行而不触发任何总线事务,并且结果行将处于修改状态。如果行是共享或拥有的,则会更新行,触发一个写总线周期来使其他缓存中可能存在的任何副本失效,并将状态更新为独占。当写入一个拥有的行时,情况有些奇怪,因为一个被写入多次的较高状态被降级为独占状态,即被写入一次的状态。背后的理论是,拥有的行可能会在另一个CPU的缓存中被复制,因此必须通过总线周期来使可能在其他缓存中过时的任何副本失效。由于主内存总线假定不能区分不同类型的写入周期,因此在写侦听命中时进行失效而不是更新,以确保任何希望通过主内存写入周期将其副本转为独占状态的缓存都能够实现。一旦发生了这个总线周期,主内存就是最新的,没有必要再驱逐具有新独占行的缓存中的那行。由于在替换行时,拥有和修改状态都需要进行逐出操作,所以它们是不合适的,这使得独占成为了对拥有行进行CPU写命中后的唯一合理状态。当然,并不存在对无效位置的写命中。
无论原始状态如何,写取代命中总是会立即使被侦听的行失效。这可能会导致软件问题,就像在MESI缓存中一样,如果一个脏行或已拥有的行被失效。如果一个拥有的副本被逐出,那么同一行在其他缓存中的共享副本将被失效,这可能看起来有点浪费。这些缓存在逐出后必须返回主内存以获取它们之前具有的相同数据。此外,每次对已拥有位置的写命中周期都会发生这个过程。这可能排除了使用MOESI协议,而更倾向于使用较低的协议进行设计。MOESI的一个优点是,它不需要特殊的总线架构,只需一个优先级方案,以允许缓存在发生侦听写命中时关闭主内存。这在设计团队必须将多处理器一致性协议适配到原本没有考虑一致性的现有总线上时,是一个非常重要的考虑因素。
在侦听读命中中,修改的行将直接向请求的缓存提供数据,并且将其状态降级为已拥有。拥有的行将以相同的方式响应侦听读命中,而不会改变响应缓存的行状态。对于独占和共享位置的读侦听命中,缓存不会向总线提供数据,因为该缓存中的数据应该由所有者提供,无论所有者是否是主内存(对于独占行始终如此,但对于共享行可能是或者不是)。所有者可能是另一个缓存。然而,独占副本将降级为共享状态。如果侦听的读命中是对已拥有或修改的行的命中,数据将由缓存提供给总线,并且该行的最终状态将是已拥有。不,无效行不会发生侦听读命中。
相比MESI,MOESI协议的吸引力在于它使用直接数据干预。这加快了处理器间通信的速度,但增加了缓存复杂性,因为缓存现在必须跟踪每行的五个状态,而不是四个。这两种协议都使用标准的主内存写入周期来使其他缓存中的匹配位置失效,并且都不使用写分配,这种方式更复杂,但实际上有助于简化下面将讨论的一些协议。
4.3.4 N+1

Synapse为其N+1容错计算机开发了一种不同的协议(见表4.4)。该协议使用智能主存(在第4.2.5节中描述)并为每个潜在高速缓存行维护一个称为使用模式位的单个位,以指示该行是由主存还是由高速缓存拥有。使用模式位告诉该行是公共的还是私有的。这个额外的位消除了高速缓存需要从主内存中抢占响应系统总线周期的需要,因为如果设置了使用模式位,则主存将禁用自己的响应,从而比串联或其他成本较低的禁止内存方案节省了一些时间。 N + 1的主存还通过在所有内存板上使用15个条目队列来加速其自身的性能。
N+1系统中的每个CPU卡都包含一个16K字节的写回物理缓存,其中每行有两个状态位:有效位和数据修改位。有效位用于表示缓存行的有效性,而数据修改位用于跟踪主存中数据的一致性。Synapse声称能够在系统上支持多达28个处理器,每个处理器都会逐渐增加系统的吞吐量。
该协议只使用了三种状态:无效、有效和脏。这三种状态有效地实现了其他四状态一致性协议所能实现的功能(有些将这些状态称为无效、共享和修改,或者MSI)。对三状态方法的支持采用了两种形式。首先,对于所有写未命中和写命中到有效位置的写入周期,都使用了写分配,这样可缓存地址的总线写操作仅仅是因为驱逐或读取嗅探命中导致的。其次,总线允许两种类型的读取周期:公共读和私有读。公共读周期仅用于读未命中周期的行更新,其中主内存使用模式位保持复位(表示主内存仍然拥有缓存行),并且其他有效副本可以存在于其他缓存中。私有读周期适用于所有写分配周期,以及特别为写未命中周期或写入到有效位置并转为脏状态而产生的行更新读取周期。
该协议有一个有些奇怪的地方是,对于尚未处于脏状态的位置的写命中,缓存会使用私有读命令从主存重新读取该位置。该行先前会通过使用公共读命令进行读未命中读取。重复的读周期会在发生未命中写周期之前使任何其他缓存副本失效,并设置主存储器的使用模式位。新获取的行将作为脏行加载到缓存中。奇怪的是,这个过程没有针对大多数代码执行写周期的方式进行优化。写通常是针对先前读取过的位置进行的。对于N+1协议来说,这意味着大多数写周期会导致两次主存读周期:一次用于读取数据的公共读周期,一次用于写入的私有读周期。这和MESI所需的周期数一样多,因为MESI首先执行主存读取,然后进行第一次写入,但是N+1协议比其他一些涉及更少主存周期的协议产生了更多的总线流量。
在对N+1进行逐步分析时,我们可以看到熟悉的事实,即所有CPU读命中都立即从缓存中服务,无需总线交互或状态变化。在CPU读未命中时,如果目标行是脏行,它将被驱逐,而无论如何,公共读总线命令都会从主存中获取更新的行。新行被存储为有效。
对于CPU写命中,如果该行是脏行,则数据将被覆写,而无需状态更改或总线交互。然而,如果该行是有效行,则会再次从主存中获取,这次使用私有读周期,这将向被嗅探缓存表明它们应使其拥有的该行副本失效,并设置主存储器的使用模式位。本地缓存将立即将该行置为脏态,并将新数据覆盖该行的相应部分。类似的周期用于支持CPU写未命中周期,因为N+1协议是基于写分配的。如果未命中的行是脏行,则会被驱逐。无论现有行的状态如何,私有读都将用于执行写分配,并使该行以脏态结束,新数据覆盖主存提供的内容。所有私有读周期都在主存储器中设置使用模式位。每个潜在的缓存行都有一个使用模式位,该位告诉主存储器不响应对该地址的读取周期,而是让持有脏行副本的缓存来处理。这个额外的位使得主存储器能够比使用更典型的串联优先级机制时更快地响应读取周期。
N+1协议中的嗅探相对简单。如果被嗅探行的起始状态为无效,不会执行任何操作。如果被嗅探行标记为有效,则只会响应由私有读引起的嗅探命中,此时该行将被设置为无效。对于被嗅探的公共读命中或私有读命中的脏行,将使用间接数据干预。这意味着读取周期将被中止,具有脏行副本的缓存将其副本写回主存,并清除使用模式位,然后允许重新尝试读取,并使脏行失效(我不确定为什么对于公共读来说,脏行不会简单地降级为有效)。这就是使用模式位处理的平衡之处。当私有读将一行移入缓存时,该行被设置为脏态,并设置使用模式位。当脏行降级时,更新主存储器,并清除使用模式位。
你会注意到在表4.4中,没有考虑写嗅探命中的可能性,这有些奇怪。为什么?因为N+1协议要求进行写分配,所有对共享或其他缓存位置的写入周期都是由读取周期或使其失效引起的。这意味着在主存储器总线上的写活动仅包括驱逐脏位置(因为没有其他缓存会包含脏行的副本),以及对非可缓存区域的写入。因此,甚至无需包括处理嗅探写命中的逻辑。似乎该架构不使用直接内存访问(DMA)来处理任何磁盘I/O。
4.3.5 Berkeley
伯克利协议在表4.5中有详细的说明。它有四种状态,就像MESI协议一样,分别是无效(Invalid)、未占有(Unowned)、非独占占有(Owned Non-Exclusively)和独占占有(Owned Exclusively)。这些状态与MESI状态相对类似,唯一的区别在于非独占占有状态是共享修改(Shared Modified)状态。MESI的共享状态与伯克利的未占有状态相似(因此我们可以假设伯克利团队没有接受如果没有缓存拥有一行的副本,那么主存储器就是所有者的观念),MESI的修改状态类似于伯克利的独占占有状态,而任何两个协议的无效状态之间几乎没有什么区别。无效就是无效。但是非独占占有状态是一种主存储器与缓存副本不一致的状态,尽管可能存在多个缓存副本。


根据表4.5,对于无效或未占有状态的缓存行,CPU读未命中将仅使用常规读周期更新该行,但是对于非独占占有或独占占有状态的行,该行是脏的,必须被驱逐。这两种状态的区别在于非独占占有行可能在另一个缓存中复制,尽管它与主存储器不一致。在驱逐后,该行的更新方式与其他两种状态相同,并且在所有四个周期中,该行最终处于未占有状态。与其他缓存协议一样,对于CPU读命中,可以直接从缓存中满足请求,而无需更改状态。
读者会注意到,伯克利协议使用术语"常规读取"和"常规写入",而不是其他协议中使用的更清晰的"公共读取"和"公共写入"。意思是相同的,只是术语不同。常规读取是由CPU读未命中、对非可缓存地址的读取或DMA活动引起的读取周期。常规写入仅来自DMA活动或非可缓存写入周期。CPU写未命中会导致写分配周期,其中必须使用读取-获取所有权总线周期来使其他缓存副本无效。正如我们在Futurebus+和N+1协议中所看到的,使用写分配需要使用特殊的总线周期来支持某种类型的无效读取周期。如果被替换的行最初是非独占占有或独占占有状态,那么在CPU写未命中被服务后,该行的状态变为独占占有。
CPU的写命中与我们所研究的其他协议处理方式不同。如果缓存行在开始时是未拥有或非独占拥有状态,其他副本将需要被作废,因此缓存发送一个用于作废的写请求周期到总线上,就像在MESI或MOESI的写一次周期中发生的那样。作废写请求周期是一种快速写操作,不会更新主存,但会作废其他缓存中匹配的缓存行,就像Futurebus+中的作废信号一样。然后,该行的状态被更改为独占拥有。如果该行最初是独占拥有状态,则只需进行更新而无需任何总线交互。如果该行最初是无效状态,则无法进行CPU的写命中周期。
除了特殊的读写操作外,Berkeley协议还需要支持两个总线机制:写分配和直接数据干预。正如我们在其他协议中看到的那样,写分配需要两种类型的读周期:常规读取和读取以获取所有权。当正在进行写分配周期时(即缓存正在更新从写失效周期中合并进来的数据的缓存行),会发出读取以获取所有权。直接数据干预用于允许拥有缓存向请求缓存提供替换的缓存行,供两种类型的读周期使用。由于存在两种类型的读周期和三种写周期,Berkeley协议中的嗅探涉及很多可能性。这两种读取是常规读取,用于在CPU读取缺失和DMA读取周期时替换缓存行;以及读取以获取所有权,用于支持写分配的读取部分,并且必须导致作废其他缓存副本的请求行。三种写周期是常规写入,由CPU用于写入非缓存地址和DMA设备;用于将缓存行从非独占状态变为独占状态并作废系统中任何其他缓存副本的作废写请求;以及在驱逐过程中用于将需要替换的脏缓存行移回主存的不作废写请求。
在常规读取周期中,仅会对属于被嗅探缓存拥有的缓存行进行响应。这意味着响应将来自于具有该行拥有非独占或拥有独占状态的缓存。在缓存禁用主存响应之后,数据从缓存传输到读取设备,缓存行的最终状态是拥有非独占状态。在这些干预周期中,主存不会更新。这允许在必要时进行快速的缓存之间的传输,但将慢速主存写入的数量减少到绝对最低限度。对于嗅探的作废请求周期,相同的过程发生,只是该行的最终状态是无效的,即使该行最初处于未拥有状态。
嗅探的常规写入和作废写请求周期都会导致任何状态的缓存行变为无效。尽管系统支持比CPU最短的写周期更长的缓存行,但不会丢失数据,因为作废写请求周期仅会对在发出作废写请求命令的缓存中完全匹配的缓存行进行命中。由于它们仅在CPU对原始缓存中的缓存行进行写命中时才会发出,因此作废写请求命中只会遇到处于非独占状态(未拥有和拥有非独占状态)的缓存行。如果该行存在,则在任何其他缓存中不能存在独占拥有状态。因此,常规写入和作废写请求周期之间的区别仅在于主存对它们的响应方式。作废写请求周期不需要主存自身更新,因为该写周期结束时,该行已经由缓存拥有,所以唯一需要持续足够长时间以便主存能够接受副本的是常规写入。
在没有作废的情况下,只有具有未拥有状态的匹配行的缓存可以进行嗅探,因为发出方的缓存行处于拥有非独占或拥有独占状态。在未作废周期的嗅探命中期间,被缓存行的所有权将从拥有该行的缓存移至主存,因此具有未拥有行的缓存对此事务实际上不需要关心。因此,在嗅探的情况下,没有对未作废写请求周期的响应。
其中三种写周期中的两种,作废写请求和无作废写请求,用于缓存数据传输。作废写请求在对未拥有或拥有非独占行的写命中周期中使用,允许作为写入处理器将其自己的缓存行副本转变为拥有独占状态时作废所有嗅探缓存的匹配行。无作废写请求在读取或写入缺失周期中替换拥有独占或拥有非独占行的驱逐过程中使用。无作废写请求周期利用了写入缓存明确知道不存在其他缓存副本的事实,因此它不会打断其他缓存的嗅探周期,因为它预先知道这些周期对除了减慢其他处理器之外不会产生任何影响。如果在多路复用的缓存标记RAM设计中实现了Berkeley协议,并且每次需要嗅探缓存标记RAM时处理器都会停止运行,这一点非常重要。
下面是两个Berkeley协议缓存如何相互作用的示例。假设在一个以Berkeley协议为基础构建的系统中只有两个CPU/缓存子系统。同一行可以在这两个缓存中以Unowned/Unowned或Unowned/Owned Non-Exclusively的方式处于Valid状态。在第一种情况下,主存将包含最新版本的缓存行,但在第二种情况下,两个缓存都包含比主存中更更新的数据。当该行被替换时,拥有Non-Exclusively副本的缓存需要更新主存。因此,总结一下,CPU的写未命中会导致Read-for-Invalidation。如果命中的行不处于Owned Exclusively状态,则CPU的写命中会导致带有Invalidation的Write操作,并且如果该行处于Owned Exclusively状态,则不会引起总线周期。读未命中会导致常规读取周期,并且如果可能,会进行直接干预。驱逐通过无Invalidation的Write操作来处理。对于Owned Exclusively行的常规读取命中会导致干预,并且该行的状态会降级为Owned Non-Exclusively,但如果该行处于Unowned或Owned Non-Exclusively状态,则数据由主存提供且状态不变。Read-for-Invalidation的嗅探命中会导致作废。常规写入和Write-for-Invalidation的嗅探命中会导致作废,并且如果它们引起对Unowned行的嗅探命中,则会忽略无Invalidation的Write操作,这是该周期唯一能够触发的嗅探命中类型。哇!
4.3.6 University of Illinois


伊利诺伊大学协议(表4.6)支持与Futurebus+、Berkeley和MOESI协议类似的直接数据干预。与Berkeley、N+l和Futurebus+类似,这需要比MOESI更多的总线支持,因为伊利诺伊协议具有读取-所有权周期、传统读取周期和使正在读取的缓存得知所读取的行是来自另一个缓存还是来自主存储器的周期,以及一个信号,它类似于Futurebus+中的tf*信号。在这个系统中,主存储器被设计为对同时的读取和写入命令作出响应,就像对简单的写入周期一样,因为在干预过程中,这就是伊利诺伊协议通过同时在总线上放置这两个周期来支持的。此外,与N+1中一样,使用写入分配来消除各个缓存需要监视总线写入流量的需要。伊利诺伊协议中使用的四个状态为无效(Invalid)、共享(Shared)、有效-独占(Valid-Exclusive)和脏(Dirty),它们都与MESI协议中的相应状态非常相似。唯一支持多个缓存中同一行副本的状态是共享状态。从无效状态开始,假设我们遇到一个CPU读取未命中的周期。缓存向所有其他缓存和主存发出标准的读取周期。主存具有最低的响应优先级,第一个接收到嗅探命中的设备将把数据传输给请求的缓存,即使该行只处于共享状态。如果该设备具有有效-独占或脏副本,它会将该行的状态降级为共享。此外,如果嗅探到的副本是脏的,则响应的缓存会通过使用主存写入周期将该行的副本同时复制回主存储器,这与引起嗅探命中的读取周期同时进行。发起请求的缓存将看到数据不是来自主存储器,并将该行的状态设置为共享。如果没有其他缓存响应,主存储器将向请求的缓存提供数据,请求的缓存看到信号表明副本来自主存储器,并将其行的状态设置为有效-独占。与Futurebus+一样,嗅探命中的通知可以消除对某些行执行一次写入的需要,从而减少总线流量。对于共享或有效-独占位置的CPU读取未命中处理方式与刚才描述的CPU读取未命中相同。然而,如果未命中的行是脏的,将引发一次驱逐,然后再进行相同的CPU读取未命中周期。与任何其他缓存一样,读取命中不会导致任何总线活动,并且命中行的状态不会改变。一个稍微不寻常的可能性是,共享行可以被覆盖,而其他处理器却没有注意到缓存行减少了一个副本。这意味着有时处理器将拥有缓存行的唯一副本,并且将该行标记为共享状态,即使它可以合法地将该行的副本标记为有效-独占状态,这将使其在需要对该行进行写入时能够进行更快的写入周期。尽管这可能会对性能产生极小的影响,但它不会影响一致性协议的可靠性。
由于缓存使用写入分配策略,所有CPU的写入未命中周期与其相应的读取未命中周期类似,唯一的区别是使用了一个读取-所有权周期来表示请求的缓存打算立即将该行置为脏状态。如果该行已经被缓存,包含该行副本的优先级最高的缓存将向请求的缓存提供该行,并且所有遇到嗅探命中的缓存都将立即使自己的副本无效。与在本文中讨论的大多数其他一致性协议不同,在一个缓存中将一个缓存行从脏状态转换为另一个缓存中的脏状态不是一个问题的迹象。如果最短的CPU写入周期小于缓存行长度,则通常使用写入分配方案,因此一个缓存/CPU只能写入到一个线路的最低有效字节或字,而下一个缓存/CPU将写入到更高有效字节或字。另一个关于从脏状态转换为脏状态的特殊之处在于,在这种情况下不需要更新主存储器。如果在主存储器中没有构建反射功能,就没有理由期望主存储器保存该线路临时版本的副本。
只有一种类型的CPU写入命中周期会产生总线活动,那就是对共享位置的CPU写入命中。当处理器写入一个已经处于共享状态的行时,缓存控制器会在主存储器总线上发送一个使所有其他共享副本立即无效的无效周期。根据伊利诺伊协议的性质,除了共享状态之外,其他状态都不会存在匹配的行。发送无效信号的缓存现在可以将其行置为脏状态。对有效-独占或脏位置的CPU写入命中将导致立即进行写入周期,并且在脏状态下结束周期。
由于使用了写入分配,嗅探写入命中根本不会发生。任何被写入的内容要么位于不可缓存空间,要么是因为驱逐了一个脏行而被写入,而且一个脏行只能在一个缓存中复制,所以不会发生嗅探命中。
4.3.7 Firefly

DEC Firefly是一种不寻常的实现,因为它从未使用无效状态。如表4.7所示,只有三种状态:共享、有效排他和脏。没有无效状态的缓存是如何工作的?它类似于我们在2.1.3节中查看的缓存,该缓存根本不使用状态位,但通过禁用缓存直到启动例程已给所有缓存行一个替换机会,保证所有位置在冷启动后都是有效的。与MESI和MOESI不同,来自一个CPU /缓存的写周期不会导致其他缓存中匹配地址的失效。相反,Firefly协议要求被嗅探的缓存在嗅探写命中周期期间更新其内容。所有对于本地缓存中不处于独占状态的缓存位置的CPU写周期都会被传播到主内存总线上。因此,对于在共享缓存中未处于独占状态的缓存行的CPU写周期会更新主内存和所有被嗅探的缓存位置。这是广播,并且在4.2.5节中描述过。这个特殊的协议需要一个总线信号来指示另一个处理器的缓存中发生了嗅探命中(与Futurebus+和伊利诺伊大学协议一样),并且与Futurebus+、伊利诺伊大学协议、伯克利和N+l一样,Firefly使用写分配来减少处理写嗅探命中的复杂性。所有总线写周期都可以分为以下三类:1)它们不在可缓存区域内,因此一致性不是问题;2)它们是对于共享缓存行的,因此被具有相同缓存行的共享副本的其他缓存观察到;或3)它们来自脏行的淘汰,因此没有其他缓存可以拥有副本。任何观察到嗅探命中的缓存将不仅告诉其自己的缓存控制器发生了这种情况,还会像Futurebus+中的tP信号那样向整个总线发出信号。这使得起始事务的CPU /缓存可以确定如何处理缓存行。与本章讨论的大多数一致性协议不同,只使用一种读取和写入周期,整个协议通过单个嗅探命中信号处理。 您现在已经注意到,这个缓存与许多其他协议一样,表现出好像它是两种不同类型的缓存,具有两种不同类型的写策略,这取决于正在写入的线路的当前状态。标记为共享的行被视为写通,除非没有其他缓存响应(通过嗅探命中)到这些位置之一的主内存写周期。任何被标记为有效独占或脏的行都被视为如果它在单处理器回写缓存中,直到遇到读嗅探命中。这与4.2.3节中讨论的通过写通策略轻松确保一致性与通过复制回缓存提供的总线带宽的改进的讨论相吻合。只有真正共享的缓存行将使用宝贵的总线带宽。与伊利诺伊协议一样,由于读取嗅探命中而干预的尝试主存读取周期会立即导致驱逐缓存在同一内存周期内启动写命令。这意味着主内存必须响应组合读取和写入命令,就好像它只是一个写周期。总之,缓存的行为就像对于在其他缓存中复制的行是写通的。如果没有其他缓存的缓存副本,则缓存使用复制回写策略。所有嗅探命中都通过嗅探缓存标记标记来响应和主存总线。所有被嗅探的缓存同时提供该行。总线冲突不是问题,因为所有被嗅探的缓存都在同一个总线周期内响应。对共享位置的嗅探命中不会改变位置的状态。对有效独占或脏位置的嗅探命中会将这些位置降级为共享状态。通过任何共享读取或写入活动期间缺乏嗅探命中,行可以升级为有效独占,并在写分配的读周期期间自动加载为脏。对于有效独占线路的CPU写命中周期将升级这些线路的状态为脏,而无需总线交互。
4.3.8 Dragon


施乐帕克(加利福尼亚州的帕洛奥多研究中心,同样也是给我们带来了苹果麦金塔的图形用户界面)设计了一种名为龙(Dragon)的多处理协议(表4.8)。该协议是另一种四态版本(共享干净、共享脏、有效独占和脏),但它与Firefly和Futurebus+类似,使用一个信号来指示总线上的嗅探命中,并且像Firefly一样,它没有无效状态。与Firefly和伊利诺伊大学协议不同,系统总线读取周期不会突然转换为主存写入周期。在系统总线上出现的唯一内存写入周期是非可缓存的写入和驱逐操作。驱逐操作可能导致两种状态:共享脏和脏。它们确实是所有权状态,但在开发这个术语时似乎小心地避免了使用"owned"这个词。
总线确实需要支持一种第三种写入周期,即更新其他高速缓存而不是主存的周期。通过使用这种比主存写入周期要快得多的写入周期,高速缓存行的所有者可以使用广播方式更新所有其他相同高速缓存行的副本,就像Firefly中所发生的那样。与Firefly和Futurebus+一样,在总线上会发出嗅探命中信号,并且每条缓存行都设置为根据总线事务中该信号的状态,在后续CPU写命中周期中使用写透或写回协议。
按步骤执行协议时,高速缓存读取未在其他高速缓存中产生嗅探命中的缺失将作为有效独占行加载到高速缓存中。如果它们产生嗅探命中,则以共享干净(即未拥有)状态加载这些行。响应缓存将其行从有效独占状态转换为共享干净状态,或者从脏状态转换为共享脏状态,但如果嗅探到的行已经是共享干净或共享脏,则其状态不会改变。如果被嗅探到的行处于脏状态,则读取周期将从被嗅探到的高速缓存中满足。如果被嗅探到的行最初是有效独占或共享干净状态,则被嗅探到的高速缓存不会在总线上放置数据,而是将该任务交给主存处理。当然,总线经过设计允许被嗅探的高速缓存禁止主存响应。最后,如果CPU的读取缺失针对本地高速缓存中的脏或共享脏行,则必须将该行驱逐。自然地,读取命中不会产生总线流量,并且可以从本地高速缓存中满足,而不会引起状态改变。
对于CPU的写入缺失,由于采用了写分配方案,其步骤与上一段详细描述的读取缺失相同,只是如果没有其他缓存响应,则加载的行将被标记为脏。如果其他缓存响应,请求的缓存将将其行设置为共享脏,并向其他缓存广播Cache Write周期。这是一个特殊的总线周期,将一行数据写入其他缓存,而不写入主存。作为对此写入周期的响应,任何其他共享脏副本都将更改其状态为共享干净。在此分配的读取部分过程中,任何有效独占的被嗅探副本已经转换为共享干净状态,而脏的被嗅探副本已经将其状态更改为共享脏,因此在Cache Write嗅探命中中不会遇到有效独占和脏状态。
仔细观察,你会发现对于另一个高速缓存中被嗅探为脏的CPU写入缺失,首先在分配的读取过程中将脏副本转换为共享脏状态,然后在循环的Cache Write部分将其从共享脏状态改变为共享干净状态。这是一个相当复杂的两步操作!如果请求缓存中的缺失行也是脏的,那么总线流量的开销也很大。将一条脏或共享脏行的CPU写入缺失替换为从另一个缓存中读取的脏或共享脏行将需要三个总线周期。首先,必须进行驱逐以腾出空间放置新行。其次,请求的CPU/缓存执行读取周期,并将请求的数据从中间缓存读入请求的高速缓存。最后,请求的高速缓存执行Cache Write以更新满足第一个请求的中间缓存中的行!
两种写命中周期分别模拟写透和回写协议。如果该行已经是脏或有效独占状态,那么缓存就会像单处理器系统中的回写缓存一样,只写入缓存,而不会引起任何总线流量。在任一周期结束时,该行的状态将为脏。如果该行是共享干净或共享脏,则"Tite"周期将通过Cache Write周期广播到所有其他高速缓存,但不会广播到主存。在任何一个这些周期结束时,该行的状态将为共享脏,除非没有其他高速缓存响应,在这种情况下,该行可以开始像使用回写策略一样运行,并且状态立即变为脏。主存只有在从拥有缓存中驱逐脏或共享脏行时才更新,即具有脏或共享脏行的缓存。这是使用内存写入周期执行的。由于内存写入周期仅在驱逐或写入不可缓存地址时发生,因此它在被窃听的缓存中所引起的反应是直接的。在另一个缓存中,这种周期可能的唯一状态是共享干净,仅在被驱逐的行是共享脏时才会发生。在这种情况下,驱逐没有任何效果,因为驱逐的行已经与其他缓存中相同行的内容匹配。共享干净行不需要作废,就像在没有同步支持的总线上一样。脏行在另一个缓存中可能没有对应项,因为脏是独占状态。
4.3.9 Others
有几个良好的设计示例可以研究,以了解在回写多处理器系统中如何解决一致性问题的方法。这些包括商用微处理器,它们都有很好的文档,并针对这些确切的问题使用自己的解决方案,缓存控制器,以及一些可以在专业期刊上研究的小型计算机和大型机架构,比如《计算机系统交易》或甚至《电子设计杂志》。
我在这里尝试展示足够的替代方案来引发您的思考,但由于同样非常重要的原因,我避免在整本书中给出任何系统性能的具体数据。首先,您的软件与迄今为止用于运行任何缓存统计的软件都不同。这意味着,如果您使用其他人的统计数据,您将误导自己。其次,您的系统与其他系统不同,因此减少主存读写的收益程度将对您选择的读写策略、行大小甚至一致性协议非常重要。务必在承诺缓存设计之前,努力在实际系统上测量真实统计数据。任何其他方法都将是冒险。
Chapter5 INTERESTING CACHE TRICKS
每个缓存设计师都会在某个时候遇到由缓存设计方法引起的困难。可能是因为缓存无法包含原始设计中指定的所有最优雅的目标,或者可能存在实际约束,需要在一个领域使用离常规的策略,而现在在另一个领域存在逻辑上的后果。无论出于何种原因,所有设计师都知道几乎可以找到解决这类困境的答案,但通常需要以不同的方式思考问题。
本章专门介绍了一些被各种处理器设计师用于改善特定情况下系统性能的技巧。在某些情况下,设计师利用系统划分来提高性能,在其他情况下,他们解决了由于选择了其他方法而引起的问题。这里展示的所有技巧都有点超出传统范畴,并且出于这个原因,我决定它们值得额外关注。
5.1 EFFICIENTLY FEEDING A SUPERSCALAR MACHINE

作者遇到的最有趣的缓存之一是IBM为Power处理器的早期版本RS/6000设计的。这个超标量CPU有四个处理器,每个处理器可以从一个指令缓存中获取数据。如图5.1所示,指令缓存行从四个缓存中一次取出四个字,每个缓存包含主存中的每第四个字。这四个字将被输入到一种交叉点矩阵中(指令缓冲区/多路复用器网络),它会将每个指令路由到适当的处理器。
优化编译器在将适当的数据加载到缓存中起着重要作用,每个处理器加载四个相邻的字,用于每个处理器的有用指令;然而,很明显,一旦编译器遇到少于四个处理器同时工作的区域,某个处理器将不需要指令,这将导致某些缓存位置根本不被使用。这不是一个最优的情况。事实上,如果这是情况的话,处理器可以进一步解耦,每个处理器都有自己的缓存和自己的指令获取地址寄存器,并且在一定程度上独立地处理指令。超标量体系结构的一个优点是它们始终保持良好的同步,而在这里描述的更松散耦合的系统将失去这个优势。
相反,IBM的设计师决定加载四个缓存,并通过交叉点开关轮流为各个处理器提供指令。这样每个处理器看起来都有自己的指令缓存,尽管实际上采用了更高效的共享缓存方法。
那么,他们如何确保不必浪费宝贵的缓存位置来存放缺失的指令呢?首先,编译器被赋予了尽可能紧密地将指令打包到内存中的自由。如果下一步只需要三个处理器,那么只会将三条指令插入到连续的三个位置中。下一个指令集将从第四个位置开始加载,而不是第五个位置。当编译器只生成一或两条指令时,同样的情况也适用。
不幸的是,指令获取地址寄存器以四的增量移动,表示四个缓存。如何将指令获取地址寄存器与指令边界同步?这是设计中的有趣部分。

答案就在指令获取地址寄存器和每个缓存的地址输入之间放置的增量器中。这些是图5.1中标有"N+1"的小方框和下游多路复用器。通过这些增量器,指令获取地址寄存器的输出可以被看作是指向当前行,或者下一行的地址。例如,假设指令恰好以三个一组的形式出现,如图5.2所示(我们将其称为三元组)。"Xn"是针对处理器X的指令,"Yn"是针对处理器Y的指令,依此类推。为了简单起见,我们假设处理器W在连续多个指令中处于空闲状态,而X、Y和Z每个周期都接收到新的指令。我们也忽略了使用缓存中的多个路(Way)的问题,只考虑左边缓存的情况。图5.2a显示了主存如何存储这些指令,然后图5.2b展示了它们如何加载到缓存中。现在的关键是以一种合理的方式从缓存中取出指令并发送到四个处理器中。
首先,指令获取地址寄存器指向每个缓存的第一个地址。前三个缓存提供这些指令,然后通过交叉点开关将它们路由到适当的处理器:指令缓存0提供给处理器X,以此类推。在获取这些指令之后,指令获取地址寄存器不会被更新,而是将指令缓存3中的第一个地址的指令通过交叉点开关发送到处理器X,并且所有三个增量器都将指令获取地址寄存器的增加输出定向,以便从指令缓存0、1和2的第二个地址输出到交叉点开关,从而允许指令缓存0提供给处理器Y,指令缓存1提供给处理器Z。这样,三元组不必对齐到四条指令的边界,缓存内存位置能够高效利用。
至于RS/6000的数据缓存有多复杂,可以不必担心。它是一个简单的四路组相联的虚拟写回缓存,每行大小为128字节。并发的行写回由读缓冲和写缓冲管理。
尽管缓存比虚拟地址页长度更深,但通过使用规则来避免数据缓存别名问题。这个规则是,通常会将两个虚拟地址位翻译成不同的物理地址,但这两个虚拟地址位必须与物理地址位完全匹配。重叠的组地址和页位始终保持相同。这是由软件管理的,这是在系统设计中同时控制硬件和软件的好处之一。
5.2 PRIMARY AND SECONDARY CACHES ON THE SAME CHIP
许多系统设计师认为,一级缓存必然是"芯片内部",而二级缓存必然是"芯片外部"。这与事实相去甚远。在第2章中提到了一个使处理器芯片配备两级缓存的很好的理由-小缓存需要更简单的地址解码逻辑,这可能使得使用小型一级缓存有助于提高芯片的整体运行频率。设计师现在给出的另一个理由是,如今庞大的芯片尺寸需要很长时间来穿越,所以在设计中散布若干小型缓存可能会以更高的时钟频率执行,而在类似的设计中使用一个大型集中式缓存无法达到这样的频率。
最近,爆新科技在其Power PC处理器的较高速度版本中提出了另外两个充分的理由。爆新的架构师指出,缓存控制器越复杂,操作越慢,但这种复杂性是可取的。他们的方法是将所有复杂性放入二级缓存中,并使一级缓存简单化,但设计成能够达到最高时钟频率。在本文撰写时的设计中,一级缓存的访问时间为0.8纳秒,而二级缓存的访问时间为1.3纳秒。导致这种差异的一个因素是,一级缓存被设计为具有较高的功耗预算和快速访问,而二级缓存则被设计为以较低的功耗运行,但速度较慢。
其次,由于片上芯片必须中断执行嗅探周期,因此在嗅探到需要进行一级失效(见第4.1.1节中关于包含性的描述)时,将这些嗅探操作放在二级缓存上而不动一级缓存/ CPU复合体是有意义的。如果二级缓存位于与处理器和一级缓存相同的芯片内部,可以在二级缓存嗅探确定需要进行失效周期后最小化时间开销的情况下强制进行一级缓存失效。设计师还评估了其他方法,包括在一级缓存上使用双端口缓存标签RAM,但这消耗了太多的芯片面积并增加了标签的复杂性,从而减慢了设计速度;另一种方法是使用带有嗅探标签的较大一级缓存,但这种方法在芯片面积上成本过高,不值得采用。

日立展示了一种提高处理器Ie上SRAM内存速度的方法,该公司将其称为分离位线内存层次结构架构(SBMHA),但我们不会用这个难以控制的名称来提及它。该项目的内存设计师们意识到,将数据输入和输出SRAM的过程中,很大一部分延迟来自于需要驱动内存阵列中的长线。通常,内存阵列按地址顺序布局,较低地址位于阵列的一端,而较高地址位于另一端。其中一端比另一端更靠近读/写放大器,这一端始终具有性能优势。日立将位线(见图5.3)在长度上进行了分割,并使较快的一端成为较小的一级缓存,而较远的一端则充当二级缓存。需要使用复杂的映射来重新排列地址,以使其有意义,但这可以轻松完成。当需要在一级和二级缓存之间移动数据时,位线会重新连接,并以较慢缓存的速度完成事务。由于长位线类似于电容负载,位线越长,消耗的功耗就越大。日立方法的一个优点是,在主缓存命中时,由于有效的位线较短,阵列的功耗显著降低。在日立的芯片中,一级阵列的位线长度与二级阵列的长度相差一个数量级。
5.3 CONTROLLING THE CACHE'S CONSISTENCY WITH ITS WRITE BUFFERS
自从i486诞生以来,英特尔的Intel Architecture芯片就有了高速缓存和写缓冲区。写缓冲区存在着保存已更新数据的风险,这些数据后续可能被处理器使用。这会引发两种问题:
-
写缓冲区中的数据未被拷贝到高速缓存(在写入周期期间发生了缓存未命中),而处理器在缓冲区更新主内存之前想要读取缓冲的内存位置。
-
写缓冲区中的数据与高速缓存中的数据一致(在写入周期中发生了缓存命中),处理器可以直接从高速缓存中复制需要的行。如果高速缓存行没有更新,则将获取陈旧的数据。
针对这个困境,有几种可能的方法。一种简单的方法是让每个单独的写周期覆盖匹配缓存行(其设置位匹配的行)。虽然在直接映射实现中这样做是可行的,因为只有一个路需要更新,但在英特尔的四路设计中变得不可能,因为所有四路都必须变成相同的行,除非设计者希望在每个四路上都产生等待状态来检查匹配。另一个更为激进的方法是使带有匹配集地址的行无效。这意味着每个单独的写周期都会使四行缓存数据失效,这不是保证有用数据留在缓存中为处理器服务的好方法。相反,在写未命中的情况下,直到写缓冲区为空,后续读取或缓存更新才会被满足。
在第二种情况下,我们在写入周期中进行缓存命中。上面提到的问题似乎不应该是一个大问题,因为要写入高速缓存的数据应该能够直接进入缓存行中。然而,由于标记查找需要一定的时间,这会破坏时序。此外,如果要写入某个小于整个高速缓存行的数据(这些高速缓存使用多字线,而处理器支持单字节写入),则高速缓存静态随机存储器(SRAM)的复杂性必须增加,以支持对线的任意部分进行短写入周期。SRAM越复杂,它的速度就越慢。英特尔解决这个问题的简单方法是,每当有写命中时就使匹配的高速缓存行无效。这似乎是一个会减慢速度的解决方案,但它可以弥补其他地方的问题。
最后一个问题是,如果因为缓存行被覆盖而需要读取不在高速缓存中的数据,怎么处理写缓冲区中的数据?一些缓存使用一种特殊的写缓冲区,拦截读周期并满足那些包含在写缓冲区中的内容的读周期。这需要写缓冲区对缓冲区中写入的每个地址都包含一个比较器,使得写缓冲区比没有此比较器时大得多。此外,写缓冲区可能只持有请求行的一部分(例如,英特尔的写缓冲区仅保存单个字节)。由于这个原因,写缓冲区保持简单,并且后续读未命中,如上所述,强制等待写缓冲区传输到主内存,然后才允许正常读周期进行。
5.4 ACHIEVING A TRUE LRU BY USING A STACK
IBM实现了一个巧妙的缓存芯片,基于高速DRAM。该芯片包括一个高速的全相联SRAM主缓存(IBM称之为行缓冲区),以及一个更大、速度较慢的两路DRAM次级缓存,都集成在同一芯片上。尽管除了使用DRAM以外,芯片的设计大部分相对简单,但设计者们采用了一种新颖的方法来生成一个真正的LRU替换机制,用于全相联主缓存。
正如在第2章中所提到的,设计更高级联缓存的一个较为困难的方面是确保被替换的行对处理器最无用。理想的缓存应该是全相联的,并使用真正的最近最少使用算法来确定要替换的行。正如我们所知,这需要大量的位数来实现。IBM的方法确实有效,但必须付出大量的工作以满足处理器的速度要求。

图5.4只显示了缓存的主部分,即行缓冲区。有32行,每行长32字节。每个行通过一个32x25位的地址转换CAM来表示,该CAM将25位的处理器地址转换为所需的5位行地址。
LRU堆栈也是一个小型的CAM(32x5),地址转换CAM的输出被发送到LRU堆栈,确定哪个LRU堆栈位置包含重复的行号。一旦在LRU堆栈中找到此行地址,堆栈位置上方的所有行地址都会向下移动到该地址,标记它们的使用次数比当前行地址少。然后,当前行地址被加载到该堆栈的第一个条目中,使得堆栈中的行地址按照真正的使用顺序列出。这需要在单个缓存访问周期内执行位移操作,这种方法相对耗电。幸运的是,只有32个位置,每个位置只移动5位,因此功耗并不高。
现在,每次替换时最佳的行地址都将落到堆栈底部。
与第2.2.2节中关于关联性讨论相比,我们展示了每个缓存行需要N!位来存储真正的LRU,而这种方法只使用32x5位,即160位。由于这个缓存中只有相当于一行,而且这一行有32路关联性,传统的方法需要表示32!个状态,可以用log2(32!)位编码,或者118位。所使用的位数之间的差距仅为26%,足够小,我们应热烈欢迎使用更直观的基于堆栈的方法,而不是使用更深奥的最小编码技术。
5.5 A "SEMIASSOCIATIVE" CACHE

这是一个IBM构思的相对复杂的混合缓存:一个完全关联的缓存和一个八路组相联的缓存。它展示了当你将几乎无限的智力投入到有限的晶体管数量中时会发生什么。缓存的基础是一个八路组相联的物理缓存,使用单字线。这没有什么特别之处。问题在于增加了一个基于CAM设计的额外目录。这在图5.5相对复杂的图表中有所展示。通过添加CAM,缓存的访问时间不会受到大多数多路缓存中路径选择机制延迟的影响。
CAM在虚拟地址位的八位子集被翻译为实际地址之前查看。缓存的CAM内容类似于MMU的页表CAM内容。通过在地址被MMU翻译之前在额外的CAM目录中查找虚拟地址,缓存数据RAM的路径选择可以提前进行,从而在大多数情况下可以得到预期的正确数据路径(毕竟,我们在处理统计数据)。在这种应用中,使用CAM而不是缓存标记存储器的一个优点是,当虚拟地址呈现给阵列时,搜索即刻开始,而不需要等待地址解码的发生。每一行的每个标记与其他标记同时查看虚拟标记位。当然,数行的每个路可能都包含匹配的虚拟地址位,因此六个组地址被解码,只有与匹配的组地址相关联的CAM虚拟地址匹配才被允许路由到数据RAM的路选择多路复用器。
在进行CAM比较时,缓存标记RAM和缓存数据RAM也会解码六个组地址位。当缓存数据RAM的八个路的所有数据都准备好时,CAM搜索已经使多路复用器能够将相应路的数据传输到处理器的数据输入引脚上。如果由于某种原因在任何路中都没有匹配,CAM的下游逻辑会告诉处理器发生了缓存未命中。
同时,地址正在MMU中从虚拟地址翻译为物理地址,并且MMU的36位输出与缓存标记RAM中相应路中的36位标记位进行比较。这是验证CAM是否选择了缓存数据RAM中正确的路,甚至是否正确地识别了命中的酸性测试。以防万一在错误的路中出现了虚假命中,目录会在随后的周期内对所有八个目录路上的MMU输出进行比较。通过这样做,缓存可以迅速恢复由CAM产生的虚假命中。
总的来说,可以避免图2.11中的长延迟路径,并实现类似于图2.10中的更快路径,即使缓存实际上是一个八路组相联的设计。在这个设计中,IBM使用了二转子CAM单元构建了缓存,因此CAM的芯片面积成本大约是基于六转子单元的缓存数据RAM阵列的两倍。可以相信,没有等待状态的一定大小的八路组相联缓存性能优于具有一个等待状态的三倍大小的缓存。另一方面,有趣的是猜测,如果八路组相联设计(在没有CAM的情况下将会导致使用等待状态)让位给直接映射的零等待设计,IBM是否可以通过使用更大的零等待直接映射设计来简化。这可能取决于系统运行的软件。
5.6 CONTROLLING ALIASES BY USING EIGHT COMPARATORS
在其中一款处理器中,AMD采用了一个逻辑指令缓存,并以一种真正强力的方式解决了别名问题。缓存中设置位的数量比页面内索引地址位的数量多两位。这意味着别名的可能性存在真正的问题,正如2.2.1节所描述的那样。
缓存标记存储了被缓存位置的物理地址。当一个页面索引地址输入到缓存中时,它可能被映射到由重叠位的四种可能状态指示的任何四个位置之一。因此,要么处理器必须等待MMU翻译地址后才能确定周期是缓存命中还是缓存未命中,要么必须在MMU翻译之前评估与索引地址相关的所有四个集合地址的标记。
AMD选择了第二个方案,同时检查可能表示给定集合地址的每个可能缓存行。在每个处理器周期中,通过对缓存的两个路的每个可能命中位置(由两个重叠位确定)进行比较,同时进行八次比较。踪踪周期的处理方式也相同,需要同时完成八个踪踪周期。这种方法需要大量的逻辑,但确实解决了别名问题,而无需将缓存速度降低到MMU所需的速度。
5.7 WRITE BUFFERING VS. MULTIPROCESSING
英特尔体系结构规范涵盖自8086以来的所有x86架构版本,并致力于理解自该架构诞生以来所进行的多次升级。值得称道的是,奔腾Pro处理器及其后续设备的设计师们必须考虑到多处理、缓存、多程序运行和写入缓冲的综合效应。以下说明将展示英特尔采取的一些方法,以在所有这些需求的冲突要求中保持某种程度的合理性。
迄今为止,本文还未涉及的一个问题是多处理系统中处理器在上电复位后如何确定彼此身份。在某些系统中,每个处理器由坚固的硬件定义,因此,也许总线上的第一个处理器是调度器,离该处理器越远的位置根据其物理位置被分配一个编号。在其他系统中,处理器共享专用的通信通道,因此处理器1可以与处理器2通信,2与3通信,依此类推。最令人费解的方法(但在公开发表的论文中经过深思熟虑)是,所有处理器都有平等机会成为调度器或从属处理器。尽管英特尔还支持一种不太对称的方法,其中处理器可以在上电时硬连接为"引导处理器",这样可以简化软件开发。
对于这种类型的多处理,支持相当简单。必须将总线"锁定",以防止其他处理器在一个处理器修改该位置时访问共享位置。这样,首个访问该位置的处理器可以说"我是#1",第二个处理器可以观察到并说"那么我是#2",依此类推。为了实现这一点,需要一种机制,使处理器能够检查一个位置并确定其内容,然后修改这些内容,而不会被另一个处理器同时读取或更改。这通过"锁定"周期来处理。锁定周期涉及读取后跟写入,或在某些情况下是写入后跟读取。最典型的锁定周期是增量或减量指令(读取、增量和写入)。
如果通信位置仅在回写缓存中复制而不在主内存中,则这些指令将无法正常工作。当存在这些锁定周期时,英特尔提供了一种使处理器不向其他处理器报告这一情况的方法。读者可能觉得这很奇怪,直到反思了可以利用读/修改/写周期进行处理器之间的通信的某些情况,但被通信地址只有间断性地被其他处理器读取。由于有理由通过将这些位置转为修改状态以加速性能(这是英特尔体系结构中所倾向的方法),因此英特尔在回写缓存中添加了支持锁定周期所需的硬件。他们称之为缓存锁定。
当存在写入缓冲区时,情况变得更加复杂。在大多数带缓冲区的缓存中,写入缓冲区通常不必过多考虑数据的位置。如果缓存位置被更新,通常会在缓存中进行更新(对于回写缓存)或者在数据写入写入缓冲区时使其无效(对于写透缓存)。在分配的缓存中,首先读取数据,然后更新写入缓冲区的数据和缓存行。如果该位置恰好是处理器进行通信的位置,并且是一个页面既指定为写透又指定为回写的处理器,则会变得棘手。在英特尔体系结构中,写缓冲区中的行代表在写命中时已失效的缓存行,而且该行所在的页面被指定为写透。由于架构允许虚拟内存页面重叠,并且允许将单个页面声明为写透、回写或不可缓存,缓存控制器为重叠范围选择最简单的缓存选择。
奔腾II处理器具有16个加载缓冲区和12个存储缓冲区,每个缓冲区都可能包含与缓存内容冲突的数据,除非得到适当管理。读周期会检查写(存储)缓冲区,因此可以简化某些周期。某些读缓冲区通常用于读取猜测的分支周期,并且需要在任何时候存在疑问时清除,以防分支目标被写周期修改。所有写缓冲区都在某些情况下写入主内存,包括执行I/O周期的任何时候(以防I/O周期启动需要当前主存储器的DMA)。有一个"组合写入"周期,将写入周期组合成单个缓存行写入。这通常用于未缓存的空间中写入屏幕内存。为了避免进一步复杂化处理器排序,不使用字节聚集。写分配用于在写失误周期中替换行。当对未缓存的地址执行锁定周期时,清除缓冲区,以便在读/修改/写周期中读取和写入周期发生时没有任何歧义。换句话说,在这种情况下,处理器从弱排序移动到强排序。如果将地址缓存为写透,或者如果缓存行为回写但处于共享状态,则会标记缓存副本无效。如果该行位于回写位置并被标记为排他性,则将其带到已修改状态,并且不缓冲写入。如果这似乎困难,请考虑作者选择不尝试绘制该过程的复杂性事实。它无法适合第4章中找到的类型的图表中的任何一个。虽然确定这些情况的细节可以让人头昏眼花,但最终要考虑每种情况,确定每种情况的适当响应,然后实施状态机(它总比挑战所示的小)以使这些状态相互匹配。英特尔决定允许的另一个奇怪情况是自修改代码情况。尽管指令缓存不需要写协议,但数据缓存有可能包含对指令缓存中代码的更新。仅出于这个原因,英特尔架构具有钩子,帮助指令缓存窥视数据缓存,并反映程序可能想要对代码造成的任何更改。