1. 引言
缓存是指地址离开处理器后遇到的最高级或第一级存储器层次结构。
如果处理器在缓存中找到了所请求的数据项,就说发生了缓存命中。如果处理器没有在缓存中找到所请求的数据项,就说发生了缓存缺失。此时,包含所请求的字的固定大小的数据集(称为块)被从主存储器中检索出来并放入缓存中。时间局部性告诉我们:很可能会在不远的将来用到这个字,所以把它放在缓存中是有用的。
缓存缺失需要的时间取决于存储器的延迟和带宽。延迟决定了提取块中第一个字的时间,带宽决定了提取这个块中其他数据的时间。缓存缺失是硬件处理的,而且会导致顺序执行暂停或停顿,直到数据可用。对于乱序执行的处理器来说,需要使用该结果的指令仍然必须等待,但其他指令可能在缓存缺失期间继续执行。
地址空间通常被分为固定大小的块,称为页。当处理器引用的一个页即不在缓存中也不在主存,就会发生缺页错误,整个页将被从磁盘移到主存储器中。由于处理页缺失消耗的时间太长,所以是由软件处理的,且处理器不会停顿。
1.1 缓存性能回顾
处理器由于等待存储器访问造成的停顿周期数,称为存储器停顿周期。性能为处理器周期数与存储器停顿周期数之和与时钟周期数乘积。
存储器停顿周期数取决于缓存缺失数和每次缺失代价:存储器停顿周期=缺失数*缺失代价。
在缺失时,后面的存储器可能因为先前的访问请求或内存刷新而处于繁忙状态。时钟周期的数目也会随着处理器、总线和存储器之间不同的时钟接口发生变化。所以请记住,使用常数作为缺失代价是一种简化。
缺失率分量就是缓存访问中导致缺失的访问比例。今天许多微处理器提供了用于计算缺失数与存储器访问次数的硬件,这种缺失率测量方式要容易得多。
顺便一提,每条指令的缺失数经常以每千条指令的缺失数的形式给出,以显式整数而非小数。表示为“每条指令的缺失数”的好处在于它与硬件实现无关,缺点在于每条指令的缺失数的体系结构相关。
1.2 四个存储器层次结构问题
1. 一个块可以放在缓存中的什么位置?
根据块的放置位置,将缓存组织方式分为以下3种:
a. 如果一个块只能出现缓存中的一个位置,即直接映射;
b. 如果一个块可以放在缓存中的任意位置,即全相联;
c. 如果一个块可以放在缓存中由有限个位置组成的set内,即组相联。块先映射到set,然后这个块看一看放在这个set内任意位置。通常以位选择方式来选定组;
2. 如果一个块就在缓存中,如何找到它?
缓存中每个块帧上都有一个地址标记,它给出了块地址。每个缓存块的标记包含了用于检测它是否与处理器的块地址相匹配的信息。由于速度非常重要,所以会对所有可能标记进行并行扫描,这是一条规则。
地址是如何划分的。第一次划分块地址和块偏移之间,然后将块帧地址进一步分为标记字段和索引字段。块偏移字段从块中选择所需数据,索引字段选择组,标记字段与之比较以便判断是否命中。记住:
a. 不用在对比中使用偏移量,因为对比只是判断整个块是否存在,而只有匹配的块才需要使用偏移。
b. 核对索引是多余的,因为它是用来待核对组的。
3. 在缓存缺失时应当替换那个块?
在缺失时,缓存控制器必须选择一个将被所需数据替换的块。直接映射布置方式的好处就是简化了硬件判决。只会查看一个块帧,以确定是否命中,而且只有这个块可被替换。对于全相联或组相连布置方式,在缺失时会有许多块可供选择。策略如下:
a. 随机:为进行均匀分配,候选块是随机选择的。一些系统生成的伪随机块编号,以实际可重复的行为,这在调试硬件时特别有用。
b. 最近最少使用:为了减少丢弃即将需要的信息的可能性,对数据块的访问会被记录下来。依靠过去预测未来,被替换的块是未使用时间最久的块。LRU依赖于局部性的一条推论:如果最近用过的块很可能会被再次用到,那么放弃最近最少使用的块是一种不错的策略。
c. 先进先出:因为LRU的计算可能非常复杂,所以这一策略是通过确定最早的块来近似LRU,而不是直接确定LRU;
随机替换的一个好处是易于硬件实现。随着要跟踪的块数的增加,LRU的成本也变得越来越高,通常只能采用近似法。一种近似方法是为缓存中的每个组设定一组比特,每个比特对应于缓存中的一路。在访问一个组中特定的路时候,需要将这一路对应的比特打开。如果同一个组中的所有路的比特都为打开的情况下,这一个组中除了该路的比特都应该关闭。在必须替换一个块时,处理器从相应比特被关闭的路中选择一个块,如果有多种选择,则随机选定。
4. 在写入时发生了什么?
在数据缓存通信流量中,写操作占10%而读操作占据26%。要加快常见情景的执行速度,就意味着要针对读操作优化缓存,尤其是处理器通常会等待读取的完成,而不会等待写操作。
可以在读取和比对标记的同时从缓存中读取块,只要有了块地址就开始读取。如果读命中,则立即将所需部分传送给处理器。即使读缺失,也只是增加了一点功耗。
不能对写操作应用这一优化。要想修改一个块,必须先核对标记,以检查该地址是否命中。由于标记核对不能并行执行,所以写操作需要的时间通常要长于读操作。另一种复杂性在于处理器还指定了写入的大小。通常是1~8字节,并且只能修改一个块的相应部分。写入策略可用来区分缓存设计。在写入缓存时,两种基本选项:
a. 写直达:信息被写入缓存中的块和低一级存储器中的块;
b. 写回:信息仅被写到缓存中的块。修改后的缓存块仅在被替换时才写到主存储器。
为降低在替换时写回块的频率,通常会使用一种称为“脏位”的功能。这一状态位表示一个块是脏的(在缓存中经历修改)还是干净(未被修改)。如果它是干净的,则在缺失时不会写回该块,因为在低级存储器中可以找到与缓存中相同的信息。
采用写回策略时,写操作的速度与缓存存储器的速度相同,一个块中的多个写操作只需要对低一级存储器进行一次写入。由于一些写入内容不会进入存储器,所以写回方式使用的存储带宽较少,这使得写回策略对多处理器更具吸引力。由于写回策略对存储器层次结构其余部分及存储器互连的使用少于写直达,所以它还可以节省功耗,对于嵌入式应用程序极具吸引力。
相对于写回策略,写直达策略更易实现。缓存总是干净的,所以它与写回策略不同,读缺失永远不会导致低一级存储器的写操作。写直达策略还有一个好处:下一级存储器中拥有数据的最新副本,从而简化了数据一致性。数据一致性对多处理器和IO来说非常重要。多级缓存使写直达策略更实用于高一级缓存,这是因为写操作只需要传播到下一个较低级别,而不需要一直传播到主存储器。
IO和多处理器希望为处理器缓存使用写回策略,以减少存储器通信流量,又希望使用写直达策略,以与低级存储器层次结构保持缓存一致。
如果处理器在写直达期间必须等待写操作的完成,则说该处理器处于写入停顿状态,减少写入停顿的常见优化方法是写缓冲区。利用这一优化,数据被写入缓冲区之后,处理器就可以立即继续执行,从而将处理器执行与存储器更新重叠起来。不过,即使有了写缓冲区,也会发生写入停顿。
由于在写入时并不需要该项数据,所以在发生写缺失时会有以下2种选项:
1. 写分配:在发生写缺失时将该块读到缓存中,随后对其执行写命中操作。
2. 写不分配:写缺失不会影响缓存。仅修改低一级存储器中的块。
通常,写回缓存采用写分配,希望对该块的后续写入能够被缓存捕获。写直达缓存通常使用写不分配,因为即使存在对该块的后续写操作,这些写操作也必须进入低一级存储器,那么读入缓存没意义。
1.3 举例:Opteron数据缓存
AMD Opteron微处理器中数据缓存的方式。该缓存包含64KB字节的数据,块大小为64B,采用2路组相连方式,LRU的替代策略、写回+写分配策略。
Opteron之所以没有利用虚拟地址的所有64位,只用了48位虚拟地址,被翻译成40位物理地址,是因为它的设计者认为还没有人会需要那么大的虚拟地址空间,而较小的空间可以简化Opteron虚拟地址的映射。
1. 进入缓存的物理地址被分为2个字段:6位块偏移地址,剩余34位地址进一步分为地址标记和缓存索引。
2. 缓存索引选择要检测的标记,以查看所需块是否在此缓存中。索引大小取决于缓存大小,块大小和组相联度。Opteron缓存的组相联度为2,索引计算是:
2^索引=缓存大小/(块大小 * 相联度) = 64KB / (64B * 2) = 512 = 2^9;
因此,索引宽9位,标记宽为34-9=25位。但64字节远多于处理器希望一次处理的数目。因此,将缓存存储器的数据部分安排为8字节更合理。
3. 在从缓存中读取这两个标记之后,将它们与处理器所提供块地址的标记部分进行比对。为了确保标记汇总包含有效信息,有效位必须为1,否则,对比结果将被忽略。
4. 假定有一个标记匹配,最后一步是通知处理器,使用2选1(2路)多路选择器的仲裁结果从缓存中载入正确的数据。Opteron可以在2周期内完成这4个步骤。
对于写操作而言,如果要写入的字在缓存中,那么前3步是相同的。由于Opteron是乱序处理器,所以只有在它发出指令已提交而且缓存标记比对结果显示命中的信号之后,才会将数据写到缓存中。
在缺失时会发生什么情况呢?在读缺失时,缓存会向处理器发出信号,告诉它数据还不可用,并从下一级层次结构中读取64字节。对于该块的前8个字节,延迟为7个时钟周期(?),对于块的其余部分,延迟为每8字节需要2个时钟周期(4个步骤只需2个时钟周期)。由于数据时组相联的,所以需要选择替换那个块。Opteron使用LRU,所以每次访问都必须更新LRU位。替换一个块意味着更新数据、地址标记、有效位和LRU位。
由于Opteron使用写回策略,旧的数据块可能已经被修改,所有不能随便丢弃。Opteron为每个块保存1个脏位,以记录该块是否被写入。如果被替换的块被修改,它的数据和地址将被发送到写缓冲区。Opteron能容纳8个牺牲块。它会将牺牲块写入层次结构的低一级,这一操作和其他缓存操作并行执行。如果写缓冲区已满,缓存就必须等待。
由于Opteron读缺失和写缺失都会分配一个块,所以二者的处理非常类似。
尽管可以尝试用一个缓存来同时提供数据和指令缓存,但这样它可能会成为瓶颈。例如,在执行载入或存储指令时,流水化处理器将会同时请求数据字和指令字。因此,单个缓存会成为载入和存储的结构冒险,从而导致停顿。解决这一问题的简单方法就是分开缓存:一个缓存用于指令,一个缓存用于数据,即分立缓存。
2. 缓存性能
由于指令数和硬件无关,所以使用这个数值来评价处理器性能是很有诱惑力的。但是这种间接性度量让设计师不断栽跟头,因为缓存延迟的差异性。另外,由于缺失率也与硬件的速度无关,所以评估存储器层次结构性能的相应焦点就主要集中在缺失率上。
注意存储器平均访问时间仍然是性能的间接度量;尽管它优于缺失率,但并不能替代执行时间。
2.1 存储器平均访问时间与处理器性能
一个显而易见的问题是:因缓存缺失导致的存储器平均访问时间能否用于预测处理器性能?
首先,还有其他原因会导致停顿,比如由于IO设备使用内存而引起的竞争。由于存储器层次结构导致的停顿远多于其他原因导致的停顿,所以设计人员经常假定所有存储器停顿都是由缓存缺失导致的。但在计算最终性能时,一定要考虑所有存储器停顿。
其次,上述问题的答案也取决于处理器。对于顺序执行处理器,那答案基本上就是肯定的。处理器会在缺失期间停顿,存储器停顿时间与存储器平均访问时间存在很强的相关性。
目前认为命中时钟周期包含在CPU执行时钟周期中。
对于低CPI、高时钟频率的处理器,缓存缺失会产生双重影响:
1. CPI越低,固定数目的缓存缺失时钟周期产生的相对影响越大。
2. 在计算CPI时,单次缓存缺失代价是以处理器时钟频率周期进行计算的。
尽管将存储器平均访问时间降至最低是一个合理的目标,但请记住,最终目标是缩短处理器执行时间。
2.2 缺失代价与乱序执行处理器
对于乱序执行处理器,如何定义“缺失代价”呢?是缓存缺失的全部延迟,还是仅考虑处理器必须停顿时的“暴露”延迟或无重叠延迟?
将总缺失延迟分解为没有争用时的延迟和因为争用导致的延迟,以考虑乱序执行处理器中的存储器资源争用。
1. 存储器延迟长度:在乱序执行处理器中如何确定存储器操作的起止时刻。
2. 延迟重叠的长度:如何确定与处理器重叠的起始时刻。
由于在流水线退出阶段只能看到已提交的操作,所以如果处理器在一个时钟周期内没有retire最大可能数目的指令,我们就说它在该时钟周期内停顿。
关于延迟,可以在存储器指令位于指令窗口排队的时刻开始测量,也可以从生成地址的时刻开始,或从指令被实际发送给存储器系统的时刻开始。只要保持一致即可。
乱序执行处理器存储器停顿的定义的测量比较复杂。因为乱序处理器容忍缓存缺失导致的延迟,但不会对性能造成损害。
3. 六种基本的缓存优化
分为以下3类:
1. 降低缺失率:较大的块、较大的缓存、较高的相联度;
2. 降低缺失代价:多级缓存,为读操作设定高于写操作的优先级;
3. 缩短在缓存中命中的时间:在索引缓存时避免地址变换。
将缺失分为3个类别:
1. 强制缺失:在第一次访问某个块时,它不可能出现在缓存中,所以必须将其读到缓存中。这种缺失也被称为冷启动缺失或首次访问缺失;
2. 容量缺失:如果缓存无法容纳程序执行期间所需要的全部块,则由于一些块会被丢弃掉,过后再另行提取,所以会发生容量缺失;
3. 冲突缺失:如果块放置策略为组相联或直接相联,则会发生冲突缺失。因为如果有太多块被映射到同一个组中,则这个组中某个块可能会被丢弃,过后再另行提取。这种缺失也被称为冲突缺失。要点就是:由于对某些常用组的请求数超过缓存的路数,所以本来在全相联中命中的情景会在组相联变为缺失。
强制缺失与缓存大小无关,容量缺失随容量的增加而降低,冲突缺失随着相联度的增大而降低。
从概念上来讲,冲突缺失是最容易避免的;全相联布置策略就可以避免所有的冲突缺失。但是,全相联的硬件实现成本非常高昂,可能会降低处理器时钟频率。
除了增大缓存之外,针对容量缺失没有其他解决方法。如果缓存容量远小于程序所需要的容量,很容易出现相当一部分时间用于在缓存层次结构的两级之间移动数据,即存储器抖动。由于需要太多的替换操作,所以摆动意味着计算机的运行速度接近低级存储器的速度,甚至会因为缺失代价而变得更慢。
另外一种降低3C缺失的方法是增大块的大小,以降低强制缺失数,但稍后将会看到,大型块可能会增加其他类型的缺失。
3C局限性在于无法解释各个缺失。其次,3C还忽略了替换策略,一方面是难以建模,另一方面是它总体来说不太重要。但在具体环境中,替换策略可能会实际导致异常行为,比如,在高相联度下得到较低的缺失率,这与3C模型的结果矛盾。
遗憾的是,许多降低缺失率的技术也会增加命中时间或缺失代价。
3.1 增大块大小以降低缺失率
降低缺失率最简单办法就是增大块大小。较大的块也会降低强制缺失,这是因为较大的块充分利用了空间局部性。同时,较大的块也会增大缺失代价。由于它们减少了缓存中的块数,所以较大快可能会增加冲突缺失,如果缓存很小,甚至还会增大容量缺失。
若缓存大于4KB,块大小应为64B,反之,32B即可。
在所有技术中,缓存设计者都在尝试尽可能同时降低缺失率和缺失代价。块大小的选择有赖于低级存储器的延迟和带宽。高带宽和高延迟需要采用大块,因为缓存在每次缺失时能够获取的字节多出许多,而缺失代价却增加得很少。相反,低延迟和低带宽则需要采用小块,因为这种情况下采用较大块不会节省多少时间。例如,一个小块的两倍的缺失代价才可能接近比该块大两倍的缺失代价。更多的小块还可能减少冲突缺失。
3.2 增大缓存以降低缺失率
降低容量缺失率的最明显办法是增加缓存的容量,其明显的缺点可能是延长命中时间,增加成本和功耗。这技术经常在片外缓存使用。
3.3 提高相联度以降低缺失率
缺失率与相联度的两条经验规律:
1. 对于特定大小的缓存,从实际降低缺失数的功效来说,八路组相连和全相联一样有效。
2. 即2:1缓存经验定律,大小为N的直接映射缓存与大小为N/2的2路组相连缓存具有大体相同的缺失率。
增大块大小可以降低缺失率,但会增加缺失代价;
增大相联度可能会延迟命中时间。
要加快处理器时钟周期,宜使用简单的缓存设计,但提高相联度会提高缺失代价。
对于不大于8KB,不超过4路组相连的缓存。从16KB开始,较高相联度的教程命中时间超过了因为缺失降低所节省的时间。
3.4 采用多级缓存降低缺失代价
架构师思考处理器与存储器之间性能差距:是应当加快缓存速度以与处理器速度相匹配,还是增大缓存以避免加宽处理器与主存储器之间的鸿沟?
一个回答是:二者都要实现,在原缓存与存储器之间再添加一级缓存可以简化这一决定。第一级缓存可以小到足以与快速处理器的时钟周期相匹配。而第二级缓存则可以达到足以捕获许多本来可能进入主存储器的访问,从而降低实际代价。
针对二级缓存系统采用以下术语:
1. 局部缺失率:缓存中缺失数除以对该缓存进行的访问次数;
2. 全局缺失率:缓存中的缺失数除以处理器发出的存储器访问次数。指出在处理器发出的访问,有多大比例指向了内存。
此二图说明:
1. 如果L2远大于L1,则全局缓存缺失率与第二级缓存的局部缓存缺失率非常类似。
2. 局部缓存缺失率不是L2的良好度量标准;它是L1缓存缺失率的函数,因此可以随着L1的改变而变化,所以,在评估L2时,应当使用全局缓存缺失率。
两级缓存之间的首要区别就是L1的速度影响处理器的时钟频率,而L2速度仅影响L1缓存的缺失代价。因此,在设计L2时,主要有2个问题:
1. 是否会降低CPI中的存储器平均访问时间部分?
2. 成本多高?
首要决定L2的size,由于L1的内容有可能都在L2,所以L2应该远大于L1。
还有一个问题是组相联对L2是否有意义?
现在考虑通过降低L2的缺失率来降低缺失代价了。另一个要考虑的问题的L1的数据是否都在L2中?。多级包含是存储器层次结构中一种自然策略;仅通过检查L2缓存就能确定IO与缓存之间,多处理器中的缓存之间的一致性。
包含性的一个缺点是:测量结果可能表明要对较小的L1使用较小的块,对较大的L2缓存使用较大的块。为了使包含性能够保持,在L2缺失时要做更多的工作。如果L1映射的L2被替换,则L2必须使L1的相应缓存块失效,从而会略微提升L1缺失率。为避免此类问题,许多缓存设计师使所有各级缓存的块大小保持一致。
如果设计师只能提供略大于L1的L2?此时使用多级互斥策略,即L1中的数据绝不会出现在L2中。防止L2的空间被浪费。
所有缓存设计的本质都是在加速命中和减少缺失之间平衡。对于L2,命中数要比L1少很多,所以重心偏向减少缺失。于是人们采用大得多的缓存和降低缺失率的技术,比如采用更高的相联度和更大的块。
3.5 使读缺失的优先级高于写缺失,以降低缺失代价
采用写直达缓存时,最重要的一个改进就是一个大小合适的写缓冲区。但是,由于写缓冲区可能包含读缺失所需要的位置的更新值,所以它们也会使存储器访问变得复杂。
摆脱这一困境的最简单方法是让读缺失一直等到写缓冲区为空为止。另一种方法是在发生读缺失时检查写缓冲区的内容,如果没有冲突而且存储器系统可用,则让读缺失继续。处理器几乎都采用后一种办法。
处理器在写回缓存时的写入成本也可以降低。假定一次读缺失将替换一个脏存储器块,然后将这个脏块复制到缓冲区中,然后读取存储器,在写入存储器。这样,处理器的读操作将会很快结束。
命中时间会影响处理器的时钟频率,所以它至关重要。在今天的许多处理器中,缓存访问时间限制了时钟频率。
3.6 避免在索引缓存期间进行地址变换,以缩短命中时间
根据“加快常见情景速度”指导原则,在缓存中使用虚拟地址,因为缓存命中的概率远高于缺失。这种缓存被称为虚拟缓存,而物理缓存用于表示使用物理地址的传统缓存。
区别索引缓存和对比地址是非常重要的。因此,问题是:使用虚拟地址还是物理地址索引缓存?使用虚拟地址还是物理地址进行标签比对?如果对索引和地址比对都完全采用虚拟地址,在缓存命中时就可以省掉地址变换的时间。那么为啥不是所有体系结构都构建虚拟寻址的缓存呢?
1. 提供保护。将虚拟地址变换为物理地址,无论如何都必须检查页级保护。一个解决方案是在缺失时从TLB复制保护信息,添加一个字段来保存这一信息,然后在每次访问虚拟寻址时进行核对。
2. 在每次切换进程时,虚拟地址会执行不同的物理地址,这需要对缓存进行刷新。解决方案之一是用进程识别符标记PID增大缓存地址标记的宽度。如果操作系统将这些标记指定给进程,那么只需要在PID被回收时刷新缓存。即PID可以区分缓存中的数据是不是为这个程序所准备的。
3. 操作系统和用户程序可能为同一物理地址使用两种不同的虚拟地址。被称为同义地址或别名地址。如果其中一个被修改了,另一个就会包含错误的值,而采用物理缓存是不可能发生这种情况的,因为这些访问将会首先被转换为相同的物理缓存块。
同义地址问题的硬件解决方案称为别名消去,它能保证每个缓存块都拥有一个独一无二的物理地址。软件也可以强制别名共享某些地址位,从而大大简化这一问题。
最后一个与虚拟地址相关的领域是IO。IO通常使用物理地址,从而需要映射到虚拟地址,以与虚拟地址交互。
一种使虚拟地址与物理缓存均能实现最佳性能的方案是使用一部分页内偏移量来索引缓存。在使用索引读取缓存的同时,地址的虚拟地址部分被转换,标记匹配使用物理地址。
这种方法允许缓存读操作立即开始执行,而标记对比仍然使用物理地址。这种虚拟地址索引、物理标签方法的局限性是直接映射缓存不能大于页面大小。
3.7 基本缓存优化方法小结
本节介绍了降低缺失率和缺失代价、缩短命中时间的技术。
4. 虚拟存储器
为每个进程专门分配一个完整的地址空间成本太高了,而且许多进程只使用其地址空间的一小部分。因此,必须有一种方法来在多个进程之间共享较少的物理空间。
其中一个做法就是虚拟存储器,将物理地址划分为块,并分配给不同的进程。该方法必然要求一种保护机制来限制各个进程,使其仅能访问属于自己的块。大多数虚拟存储器还缩短了程序的启动时间,因为程序启动之前不再需要将所有代码和数据都存储在物理内存中。
尽管由虚拟存储器提供的保护对于目前计算机来说是必须的,但共享并不是发明虚拟存储器的原因。如果一个程序对物理存储器来说太大了,就需要由程序员将其调整为合适大小。程序员将程序划分为片段,然后找出互斥的片段,在执行过程中根据用户程序控制来加载或卸载这些覆盖段。程序员确保程序绝对不会尝试访问超出计算机现有的物理主存储器,并确保会在正确的时间加载正确的覆盖段。这种责任降低程序员的生产效率。
虚拟存储器的发明是为了减轻程序员这一负担,它自动管理表示为主存储器和辅助存储器的2级结构。
除了共享受保护的存储器空间和自动管理存储器层次结构之外,虚拟存储器还简化了为执行程序而进行的加载过程。这种称为重定位机制允许同一程序在物理存储器中的任意位置进行。
缓存和虚拟存储器差别还有:
1. 发生缓存缺失时的替换主要是由硬件控制,而虚拟存储器替换主要是操作系统控制。缺失代价越高,做出正确决定就显得越重要。
2. 处理器地址空间的大小决定了虚拟存储器的大小,但缓存大小与处理器地址空间大小无关。
3. 辅助存储器还用于文件系统。
虚拟存储器还包含几种相关技术。虚拟存储器系统可分类为:采用固定块大小的页和采用可变大小的块的段。页大小通常在4KB~8KB,而段大小是变化的。任意处理器所支持的最大段范围是16KB~4GB,最小段是1B。
是使用页虚拟存储器还是段虚拟存储器,这一决定会影响处理器。页寻址方式有一个固定大小的地址,分为页编号和页内偏移量,类似于缓存寻址。单一地址对分段地址无效,可变大小的段需要一个字来表示段号,1个字表示段内偏移量,共计2个字。对于编译器来说,不分段地址更简单一些。
由于替换问题,今天很少只使用分段的方法。一些计算机使用一种名为页式分段的混合方式,一个段是由整数个页组成。由于存储器不需要连续的,也不需要所有段都在主存储器中,从而简化了替换过程。
4.1 再谈存储器层次结构的4个问题
问题1:一个块可以放在主存储器的什么位置?
由于涉及对旋转磁存储设备的访问,因此虚拟存储器的缺失代价非常高。如果要在较低的缺失率与较简单的放置算法之间进行选择,操作系统设计人员通常会选择较低的缺失率,因为缺失代价可能会高得离谱。因此,操作系统会允许块放在存储器中任意位置,即全相联。
问题2:如果一个块在主存储器中,如何找到它?
分页或分段都依靠一种按页号或段号索引的数据结构,其内包含块的物理地址。对于分段方式,会将偏移量加到段的物理地址中,以获得最终物理地址。对于分页方式,该偏移量只是被联系到这一物理页地址。通常根据虚拟页号进行索引,其大小就是虚拟地址空间中的页数。对于32位虚拟地址而言,假设页大小为4KB,每个页表项4字节(32位地址),页表大小为2^32/2^12*4=4MB。为了缩小这一数据结构,一些计算机向虚拟地址应用了一种散列函数,允许数据结构的长度等于主存储器中物理页的数目。也有一些使用多级页表,迅速减少页表大小。
为了缩短地址变换时间,计算机使用TLB来缓存地址变换信息。
问题3:在虚拟存储器缺失时应当替换那个块?
操作系统的最高指导原则是将缺页错误降至最低。尝试替换LRU的块,这是因为如果用过去的信息来预测未来,则将来用到这种块的可能性最低。
问题4:在写入时发生了什么?
由于访问辅助存储会耗时数百万时钟周期,还没有人构建一种虚拟存储器操作系统,在处理器每次执行存储操作时将主存储器直接写到辅存上。因此,这种总是采用写回策略。
4.2 快速地址变换技术
页表通常很大,所以存储在主存储器中,有时它们本身就是分页的。分页意味着每次存储器访问在逻辑上至少要分两次进行,第一次存储器访问是为了获得物理地址,第二次访问是为了获得数据。借助TLB,存储器访问就很少在需要第二次访问来转换数据。
TLB保存了虚拟地址部分,数据部分保存了物理页帧编号、保护字段、有效位,通常还有一个使用位和脏位。要改变页表中某一项的物理页帧编号或保护字段,操作系统必须确保旧项不在TLB中;否则,操作系统不能正常运行。
4.3 选择页面大小
选大页的理由:
1. 页表大小与页面大小成反比,因此,增大页面大小可以节省存储器;
2. 页面较大时,缓存可以更大,缓存命中时间可以更短;
3. 从辅助存储传递较大页面的效率更高;
4. TLB条目有限,所以页面较大意味着可以高效地映射更多存储器,从而减少TLB缺失数量;
采用小页的理由:
1. 节省存储。当虚拟存储器的连续区域的大小不等于页面大小的整数倍时,采用较小的页可以减少存储的浪费,内部碎片化越小。
2. 进程启动时间,许多进程很小,所以较大的页面可能会延长调用一个进程的时间。
4.4 虚拟存储器和缓存小结
对于一个64位虚拟地址到41位物理地址的虚构示例。它采用2级存储,L1的缓存大小和页面大小都是8KB,虚拟地址索引,物理标签。L2缓存为4MB。这二者的块大小都是64B。
首先,64位虚拟地址在逻辑上被划分为虚拟页号和页内偏移量。前者被发送到TLB,并被转换为物理地址,而后者被发送给L1缓存,充当索引。如果TLB匹配命中,则将物理页号发送到L1缓存标记,检查是否匹配。如果匹配,则是L1缓存命中。块偏移随后为处理器选择该字。
如果L1缓存核对显示不匹配,则使用物理地址尝试L2缓存。物理地址的中间部分用作4MB L2缓存的索引。将所得到的L2缓存标记与物理地址的上半部分对比,以检查是否匹配。如果匹配,我们得到一次L2缓存命中,数据被送往处理器,处理器根据使用块偏移量选择所需字。在发送L2缺失时,会使用物理地址从存储器获取该块。
图中简化了L1被分立为数据和指令部分,导致有两个TLB。第二个简化是所有缓存与TLB都是直接映射的。n路组相联,则会将每一组标记存储器、比较器和数据存储器重复n次,并用一个n选1的多路选择器将数据存储器连接在一起,以选择命中内容。当然,如果缓存大小不变,那么缓存索引也会收缩至logN。
5. 虚拟存储器的保护与示例
多道程序导致了进程概念的出现。打个比方,进程看是程序呼吸的空气和生活的空间,即一个正在运行的程序加上持续运行它所需的所有状态。分时共享式多道程序的变体,要求在任何时刻都必须能够从一个进程切换到另一个进程,即进程切换或上下文切换。
计算机设计者确保进程状态的处理器部分能够保存和恢复。操作系统设计师必须确保这些进程不会干扰对方的计算。
要保护一个进程的状态免受其他进程损害,最安全的做法就是将当前信息复制到磁盘上。但是,一次进程切换可能要几秒,这对分时共享来说过长了。这个问题的解决方案是由操作系统对主存储器进行划分,使几个不同的进程能够在存储器中同时拥有自己的状态。除了保护之外,计算机还为进程之间共享代码和数据提供支持,允许进程之间相互通信,或者减少相同信息的副本数目来节省存储器。
5.1 保护进程
让进程拥有自己的页表,分别指向存储器的不同页面,这样可以为进程提供保护,避免相互损害。显然,必须防止用户程序修改它们的页表,或者以欺诈方式绕过保护措施。
向处理器保护结构中加入环,可以将存储器访问保护扩展到远远超出最初的两级(用户级和内核级)。
5.3 分页存储器举例:64位的Opteron内存管理
AMD在64位中摒弃了多个段。它假定段基址为0,忽略界限字段。页面大小为4KB、2MB和4MB。
64位虚拟地址被映射为52位物理地址,较少位数可以简化硬件。64位地址空间页表的大小是惊人的。因此,AMD64使用了一种多级层次结构页表来映射地址空间,使其保持合理大小。级别数取决于虚拟地址空间的大小。
尽管不同级别的页表字段有变化,但基本都有以下位:
1. 存在位:表明该页存在于存储器中;
2. 读写位:页是只读,还是读写;
3. 用户/管理员位:用户是访问该页,还是仅限于上面3个权限级别。
4. 脏位:该页是否已经被修改;
5. 页面大小位:最后一级是多大的页面。
6. 不执行位:防止代码在某些页内执行;
7. 页级缓存禁用位:是否可以缓存该页;
8. 页级写直达位:对数据缓存是写回还是写直达;
6. 谬论和易犯错误
易犯错误:地址空间太小
由于程序的大小和程序所需要的数量必须小于2^(32 or 64)地址大小,所以地址大小限制了程序长度。地址大小之所以很难修改,原因在于它确定了所有与地址相关的最小宽度:PC、寄存器、存储器和实际地址运算。
易犯错误:忽视操作系统一存储器层次结构性能的影响
大约有25%停顿时间消耗在操作系统的缺失部分中,或者因为应用程序与操作系统互相干扰而导致的缺失部分中。
易犯错误:依靠操作系统来改变页面大小
通过增大页面大小,甚至将其构建成虚拟地址的大小,借此发展其体系结构。不过操作系统设计人员反对增大页面大小,最终通过修改虚拟存储器系统来增大地址空间,页面大小保持不变
注意到TLB缺失率极高,所以向TLB中增加较大的页面大小。希望操作系统会将一个对象分配到最大页中,从而保留TLB条目。大多数操作系统仅在精心选择的功能中使用“超级页面”,比如映射显式存储器或其他IO设备,或者为数据库代码使用这种极大页面。
7. 结语
要制造能跟上处理器步伐的存储器系统,难度极大。而主存储的原材料与最廉价的计算机一样,使上述难度进一步加大。