在本章中,我们将研究缓存设计中使用的一些方法或“行业诀窍”。虽然没有特别复杂的设计技术被使用,但已经投入了大量思考来揭示显而易见的东西,典型的设计师最终将不得不记下许多针对缓存的经验法则,以便正确地进行缓存设计。
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的输出同步的问题。