1.写在前面
前面的博客我们已经简单的介绍完了虚拟存储,虚拟存储的一些概念,然后还有就是缓存的一些概念,今天我们来介绍一下存储层次结构的一般框架。介绍之前,我们先看看几种不同类型的存储层次结构。虽然存储层次结构的很多方面都有量的区别,但是很多决定层次结构功能的策略和特征在本质上是相似的。
在构建存储层次结构之前,我们需要解决一些问题,具体的问题如下:
2.存储层次的问题
2.1问题一:块可以被放在何处
我们已经看到,在较高存储层次结构中,块的放置可以使用一系列方案,从直接映射到组相联,再到全相联。如上所述,整个方案范围可以被认为是组相联方案的变体,其中组的数量和每组中块的数量不相同:
增加相联度的好处是通常会降低失效率。失效率的改进来自于减少竞争同一位置而产生的失效。首先,来看能获得多少性能改进。下图展示不同cache容量时,相联度从直接映射到八路组相联变化时的失效率。最大的改进出现在直接映射变化到两路组相联时,失效率减低了20%~30%。随着cache容量的增加,相联度的提高对性能改进作用很小;这是因为大容量cache的总失效率较低,因此改善失效率的机会减少,并且由相联度引起的失效率的绝对改进明显减少。如前所述,相联度增加的潜在缺点是增加了代价和访问时间。
2.2问题二:如何找到块
我们如何找到某个块取决于块的放置方案,因为这决定了可能存放位置的数量。我们可以把这些方案总结如下:
在存储层次结构中,直接映射、组相联或全相联映射的选择取决于失效代价和相联度实现代价之间的权衡,包括时间和额外开销。在片上包含二级cache允许实现更高的相联度,这是因为命中时间不再关键,设计者也不必依靠标准SRAM芯片来构建模块。除非容量很小,否则cache不使用全相联映射方式,其中比较器的成本并不是压倒性的,而绝对失效率的改进才是最明显的。
在虚拟存储系统中,页表是一张独立的映射表,它用来索引内存。除了表本身需要的存储空间外,使用索引表还会引起额外的存储访问。使用索引表还会引起额外的存储访问。使用全相联映射和额外的页表有以下几个原因:
- 全相联有其优越性,因为失效代价非常高。
- 全相联允许软件使用复杂的替换策略以降低失效率。
- 全相联很容易索引,不需要额外的硬件,也不需要进行查找。
因此,虚拟存储系统通常使用全相联。
组相联映射通常用于cache和TLB,访问时包括索引和组内查找。一些系统使用直接映射cache,这是因为访问时间短并且实现简单。访问时间短是因为查找时不需要进行比较。这样的设计选择取决于很多实现细节,例如,cache是否集成在片上、实现cache的技术以及cache的访问时间对处理器周期时间的重要性。
2.3问题三:当cache发生失效时替换哪一块
当相联的cache发生失效时,我们必须决定要替换哪个块。在全相联的cache中,所有块都是替换的候选者。如果cache是组相联的,则必须在一组的块中进行选择。当然,直接映射cache中的替换很容易,因为只有一个候选者。
在组相联或全相联的cache中有两种主要的替换策略:
- 随机:随机选择候选块,可能使用一些硬件辅助实现。
- 最近最少使用(LRU):被替换的块时最久没有被使用过的块。
实际上,在相联度不低的层次结构中实现LRU的代价太高,这是因为追踪记录使用信息的代价很高。即使对于四路组相联,LRU通常也是近似实现的,例如,追踪记录哪一对块时最近最少使用的,然后追踪记录每对块中哪一个块时最近最少使用的。
对于较大的相联度,LRU是近似的或使用随机替换策略。在cache中,替换算法由硬件实现,这意味着方案应该易于实现。随机替换算法用硬件很容易实现,对于两路组相联cache,随机替换的失效率比LRU替换策略的失效率高约1.1倍。随着cache容量变大,两种替换策略的失效率,并且绝对差异也变小。实际上,随机替换算法的性能有可能比用硬件简单实现的近似LRU更好。
在虚拟存储中,LRU的一些形式都是近似的,因为当失效代价很大时,失效率的微小降低很重要。通常提供参考位或其他等价的功能使操作系统更容易追踪记录一组最近使用较少的页。由于失效代价很高且相对不频繁发生,主要由软件来近似这项信息的做法是可行的。
2.4问题四:写操作如何处理
任何存储层次结构的一个关键特性是如何处理写操作。我们已经看到两个基本选项:
- 写穿透:信息将写入cache中的块和存储层次结构中较低层的块。
- 写返回:信息仅写入cache中的块。修改后的块只有在它被替换时才会写入层次结构中的较低层。
写返回的优点:
- 处理器可以按cache而不是内存能接收的速率写单个的字。
- 块内的多次写操作只需对存储层次结构中的较低层进行一次写操作。
- 当写回块时,由于写一整个块,系统可以有效地利用高带宽传输。
写穿透的优点:
- 失效比较简单,代价也比较小,这是因为不需要将块写回到存储层次结构中的较低层。
- 写穿透比写返回更容易实现,尽管实际上写穿透cache仍然需要写缓冲区。
在虚拟存储系统中,只有写返回策略才是实用的,这是因为写到存储层次结构较低层的延迟很大。尽管允许存储器的物理和逻辑宽度更宽,并对DRAM采用突发模式,处理器产生写操作的速率通常还是超过存储系统可以处理它们的速率。因此,现在最低一级的cache通常采用写回策略。
2.4 3C:一种理解存储层次结构的直观模型
在本节中,我们将介绍一种模型,该模型可很好地洞察存储层次结构中引起失效的原因,以及存储层次结构中的变化对失效的影响。我们将用cache来解释这些想法,尽管这些想法对其他层次也直接适用。在此模型中,所有失效都被分成以下三类(3C 模型):
- 强制失效:对没有在cache中出现过的块进行第一次访问时产生的失效,也称为冷启动失败。
- 容量失效:cache无法包含程序执行期间所需的所有块而引起的失效。当某些块被替换出去,随后再被调入时,将发生容量失效。
- 冲突失效:在组相联或者直接映射cache中,很多块为了竞争同一个组导致的失效。冲突失效时直接映射或组相联cache中的失效,而在相同大小的全相联cache中不存在。这种cache失效也称为碰撞失效。
改变cache设计中的某一方面可以直接影响这些失效的原因。由于冲突失效来自同一cache块的争用,因此提高相联度可以减少冲突失效。但是,提高相联度可能会延长访问时间,从而降低整体性能。
简单地增大cache容量可以减少容量失效,实际上,多年来二级cache容量一直在稳步增长。当然,在增大cache的同时,我们也必须注意访问时间的增长,这可能导致整体性能降低。因此,尽管一级cache也在增大,但是非常缓慢。
由于强制失效时对块的第一次访问时产生的,因此,对cache系统来说,减少强制失效次数的主要方法是增加块大小。由于程序将由较少的cache块组成,因此这将减少对程序每一块都要访问一次时的总访问次数。如上所述,块容量增加太多可能对性能产生负面影响,因为失效代价会增加。
设计存储层次结构的挑战在于,任何一个改进失效率的设计可能同时对整体性能产生负面影响。
3.使用有限状态自动机控制简单的cache
本节先对简单cache进行定义,之后描述有限状态自动机,并以使用有限状态自动机控制这个简单cache作为结束。
3.1一个简单的cache
现在我们将要为一个简单的cache设计控制器。下面是这个cache的主要特征:
- 直接映射cache
- 使用写分配写回
- 块大小为四字(16字节或128位)
- cache大小为16KiB,因此该缓存内包含1024个块
- 32位地址
- 该cache的每个块内都包含有效位和脏位
根据前面的内容,我们现在可以计算该cache地址的字段:
- cache块索引为10位
- 块内偏移为4位
- 标签大小为32-(10+4),也就是18位
处理器与cache之间的控制信号为:
- 1位读或写信号
- 1位有效信息,表示该操作是否为cache操作
- 32位地址
- 32位数据,从处理器传输至cache
- 32位数据,从cache传输至处理器
- 1位就绪信号,表示cache操作已经完成
存储器与cache之间的接口与处理器和cache之间的字段基本一致,只不过数据位现在换成128位宽。额外的存储器宽度在当今的微处理器中很常见,它处理32位或64位字的处理器,而DRAM控制器通常为128位。将cache块与DRAM的宽度相匹配可以简化设计。因此将信号设计如下:
- 1位读或写信号
- 1位有效信号,表示该操作是否为存储器操作
- 32位地址
- 128位数据,从cache传输至存储器
- 128位数据,从存储器传输至cache
- 1位就绪信号,表示存储器操作已经完成
需要注意的是,存储器接口所需的时钟周期数不是固定的。我们假设当存储器读写操作完成时,存储器控制器可以通过就绪信号将该事件通知给cache。
3.2有限状态自动机
为了设计单时钟周期数据通路的控制单元,我们使用真值表根据指令类别设置控制信号。cache的控制将会更为复杂,因为对cache的操作可能包含了一系列步骤。cache控制必须指定每个步骤中需要设置的信号以及下一个将要执行的步骤。
最常见的多步骤控制技术基于有限状态自动机,它通常以图的形式表示。有限状态自动机由一系列状态和状态之间改变的方向组成。该方向由状态转换函数定义,它是当前状态和指向新状态的输入之间的映射。当使用有限状态自动机进行控制时,该自动机的状态也指定了一系列输出,该输出时当机器处于该状态下的断言。有限状态自动机实现时通常假定所有未明确断言的输出是无效的。类似地,数据通路的正确操作也取决于这样一个事实:没有明确断言过的信号都是无效的,而不是针对该信号做无意义的操作。
多选器的控制与上述行为略有不同,因为它只选择众多输入中的一个,而不考虑该输入的0还是1.因此,在有限状态自动机中,我们总是指定所有需要关注的多选器控制的设置。当使用逻辑器件实现有限状态自动机时,可以默认将控制设置为0,也因此不需要任何逻辑门。
有限状态自动机控制器的典型实现,使用一组组合逻辑和一个保存当前状态的寄存器实现。组合逻辑的输出是当前状态的下一状态编号和被断言的控制信号。组合逻辑的输入是当前状态和任一可以决定下一状态的输入。需要注意的是,在本章的有限状态自动机中,输出只取决于当前状态,而与输入无关。
3.3使用有限状态自动机作为简单的cache控制器
简单cache控制器的四个状态:
- 空闲:该状态等待处理器发出的有效读或写信号,之后有限状态自动机跳转到标签比较状态。
- 标签比较:正如名称所示,该状态检测读或写请求是命中还是失效。地址的索引部分选择用于比较的标签。如果地址中的索引部分引用的cache块中的数据是有效的,并且地址中的标签部分与标签相匹配,则命中。如果是加载指令从选择的字中读取数据,如果是存储指令就将数据写入选择的字中。之后设置cache就绪信号。如果这是一个写操作,脏位还要设置为1.需要注意的是,写命中也需要设置有效位和标签字段,即使这看上去并不需要。这是因为标签使用单独的存储器,因此在改变脏位时也需要同时改变有效位和标签字段。如果发生命中并且当前块是有效的,有限状态自动机会返回空闲状态。失效时先更新cache标签,之后如果当前块的脏位为1,则跳转到写回状态,如果该位为0,则跳转到分配状态。
- 写返回:该状态使用由标签和cache索引组成的地址将128位的块写回存储器。只有继续停留在该状态等待存储器发出就绪信号。等待存储器写操作完成后,有限状态自动跳转到分配状态。
- 分配:从存储器中取出一个新块。之后继续停留在该状态等待存储器发出就绪信号。等待存储器读操作完成后,有限状态自动机跳转到标签比较状态。尽管我们可以不重新使用标签比较状态而跳转到一个新的状态完成操作,但是分配状态之后的操作与标签比较状态的操作有大量的重叠,包括当访问位写操作时更新块中相应的字。
这个简单模型很容易扩展至更多的状态以提升性能。例如,标签比较状态在一个时钟周期内同时做了比较操作和cache数据的读和写操作。通常会将比较操作和cache访问操作分成两个状态以改进每个时钟周期所需的时间。另一个优化是添加一个写缓存,这样可以保存脏块,之后就可以提前读取新的块,使得处理器在脏块缺失时无须等待两次存储器访问。之后cache会从写缓存中将脏块写回,同时处理器处理被请求的数据。
4.并行和存储层次结构:cache一致性
假定一个多核多处理器意味着在一个单芯片上有多个处理器,这些处理很可能共享一个共同的物理地址空间。cache共享数据引入了一个新的问题,由于两个不同的处理器保存的存储器视图是通过它们各自的cache得到的。如果没有任何额外的保护措施,那么处理器可能看到两个不同的值。并且展示了对于同一个地址两个不同的处理器是如何有两个不同的值的。这个难点通常被称为cache一致性问题。
简单来说,如果对任何一个数据项的读取都能返回该数据项最近被写入的值,则称这样存储系统一致的。虽然在直观上这个含糊而简单的定义很易懂,但实际的情况会比这个定义更加复杂。这个简单的定义涵盖了存储器系统行为的两个不同方面,这两个方面对于编写正确存储程序都非常重要。第一个方面称为一致性,定义了读取操作会返回什么值。第二个方面称为连续性,定义了写入的值什么时候会被读取操作返回。
首先看一致性。如果满足以下属性,则称一个存储系统是一致的:
- 在处理P对位置X的值写入后,P读X的值,如果在P对X的写和读操作之间没有其他处理器对X的写操作,那么本次读一定可以返回P写入的值。因此,如果CPU A在第3步后读X的值,返回值应为1。
- 如果一个处理器对位置X的读操作是跟随在另一个处理器写入X值之后,并且读操作和写操作之间有足够的时间间隔、在两次对X的访问之间没有其他写入X的操作,那么本次读操作应该返回上次写入的值。因此,需要一个机制使得在第3步CPU A将值1写入存储器位置X后,将CPU B cache中的值0替换为1。
- 对同一位置的写入操作是串行的。也就是说,两个处理器对同一个位置的两次写入操作在其他处理器看来都具有相同的顺序。例如,如果CPU B在第3步后再存储器位置X上存入值2,那么处理器永远不肯能在读X值得到2之后再次读X值得到1。
第一个属性仅仅保留了程序的顺序,我们当然希望这个属性在单处理机中是正确的。第二个属性定义拥有存储器的一致性意味着什么的概念:如果一个处理器总是读取到旧的数据值,我们就可以很清楚地说这个存储器是不一致的。
对写操作串行化的需求更加精细,但是也同样重要。假设没有将写操作串行化,并且处理器P1写入位置X,之后P2也写入位置X。串行化写操作确保每个处理器都可以在某个时刻看到P2写入的结果。如果没有串行化写操作,就可能有一些处理器先看到P2写入的结果,随后有看到P1写入的结果,最终可能保留P1写入的值。避免这种情况的最简单的方法就是确保对同一位置的所有写操作都以相同的顺序被观察到,这就是我们说的写操作串行化。
4.1实现一致性的基本方案
在一个支持cache一致性的多处理器中,cache为每个共享数据项提供迁移和复制:
- 迁移:数据项可以移动到本地cache并以透明的方式被使用。迁移减少了访问远程分配的共享数据项的延迟,也减少了共享存储器的带宽需求。
- 复制:当同时读取共享数据时,cache会在本地cache中创建数据项的副本。复制减少了读取共享数据项的访问延迟和争用。
支持迁移和赋值对于访问共享数据的性能至关重要,因为许多处理器都采用硬件协议来维护cache的一致性。维护多个处理器之间的一直性的协议被称为cache一致性协议。实现cache一致性协议的关键是追踪每一个共享数据块的状态。
最常用的cache一致性协议是监听。cache中既包含物理存储器数据块副本,也含有该数据块的共享状态,但不集中保留状态。这些cache都可以通过一些广播媒介访问,而且所有的cache控制器都可以监视或监听媒介,以确定它们是否有总线或交换机的访问所需的数据块的副本。
4.2监听协议
一种实现一致性的方法是确保处理器在写入一个数据项前可以独占访问该项。这种协议被称为无效协议,因为它在写入时使得cache中的副本无效。独占访问可确保在写入时没有该项的其他可读写副本存在:所有该项的其他cache中副本都将失效。
一种使用写回cache的监听总线失效协议的示例。为说明这个协议如何确保一致性,考虑一个写操作后跟随着另一个处理器的读操作的情景:因为这个写操作需要独占性访问,所以发出读操作的处理器cache中保存的任意副本均会失效。因此,当发生读操作时,会造成一次cache失效,cache被迫去获取该数据的最新副本。对写操作而言,我们要求写入处理器有独占访问权限,以防止任何其他处理器能够同时写入。如果两个处理器确实同时试图写入相同的数据,那么只有其中一个处理器能够赢得写权限,同时也会导致其他处理器的副本失效。其他处理器想要完成写入必须获得该数据的最新副本,这个最新副本必须包含已经被更新的值。因此,该协议还强制实现了写操作串行化。
5.谬误与陷阱
- 陷阱:在写程序或编译器生成代码时忽略系统的行为。
- 陷阱:在模拟cache的时候,忘记说明字节编制或者cache块大小。
- 陷阱:对于共享cache,组相联度小于核的数量或者共享该cache的线程数。
- 陷阱:用存储器平均访问时间来评估乱序处理器的存储器层次结构。
- 陷阱:通过在末分段地址空间的顶部增加段来扩展地址空间。
- 谬误:实际的磁盘故障率和规格书中声明的不一致。
- 谬误:操作系统是调度磁盘访问的最好地方。
- 陷阱:在不为虚拟化设计的指令系统体系结构上实现虚拟机监视器。
6.写在最后
已经介绍完了存储的相关的知识,后面的博客将要介绍并行的处理器的相关的东西。