[ZZ] Cache

http://blog.sina.com.cn/s/blog_6472c4cc0102duzr.html

 

处理器微架构访问Cache的方法与访问主存储器有类似之处。主存储器使用地址编码方式,微架构可以地址寻址方式访问这些存储器。Cache也使用了类似的地址编码方式,微架构也是使用这些地址操纵着各级Cache,可以将数据写入Cache,也可以从Cache中读出内容。只是这一切微架构针对Cache的操作并不是简单的地址访问操作。为简化起见,我们忽略各类Virtual Cache,讨论最基础的Cache访问操作,并借此讨论CPU如何使用TLB完成虚实地址转换,最终完成对Cache的读写操作。

2.1 <wbr>Cache的工作原理

Cache的存在使得CPU Core的存储器读写操作略微显得复杂。CPU Core在进行存储器方式时,首先使用EPN(Effective Page Number)进行虚实地址转换,并同时使用CLN(Cache Line Number)查找合适的Cache Block。这两个步骤可以同时进行。在使用Virtual Cache时,还可以使用虚拟地址对Cache进行寻址。为简化起见,我们并不考虑Virtual Cache的实现细节。

EPN经过转换后得到VPN,之后在TLB中查找并得到最终的RPN(Real Page Number)。如果期间发生了TLB Miss,将带来一系列的严重的系统惩罚,我们只讨论TLB Hit的情况,此时将很快获得合适的RPN,并依此得到PA(Physical Address)。

在多数处理器微架构中,Cache由多行多列组成,使用CLN进行索引最终可以得到一个完整的Cache Block。但是在这个Cache Block中的数据并不一定是CPU Core所需要的。因此有必要进行一些检查,将Cache Block中存放的Address与通过虚实地址转换得到的PA进行地址比较(Compare Address)。如果结果相同而且状态位匹配,则表明Cache Hit。此时微架构再经过Byte Select and Align部件最终获得所需要的数据。如果发生Cache Miss,CPU需要使用PA进一步索引主存储器获得最终的数据。

由上文的分析,我们可以发现,一个Cache Block由预先存放的地址信息,状态位和数据单元组成。一个Cache由多个这样的Cache Block组成,在不同的微架构中,可以使用不同的Cache Block组成结构。我们首先分析单个Cache Block的组成结构。单个Cache Block由Tag字段,状态位和数据单元组成,如图2‑2所示。

2.1 <wbr>Cache的工作原理

其中Data字段存放该Cache Block中的数据,在多数处理器微架构中,其大小为32或者64字节。Status字段存放当前Cache Block的状态,在多数处理器系统中,这个状态字段包含MESI,MOESI或者MESIF这些状态信息,在有些微架构的Cache Block中,还存在一个L位,表示当前Cache Block是否可以锁定。许多将Cache模拟成SRAM的微架构就是利用了这个L位。有关MOESIFL这些状态位的说明将在下文中详细描述。在多核处理器和复杂的Cache Hierarchy环境下,状态信息远不止MOESIF。

RAT(Real Address Tag)记录在该Cache Block中存放的Data字段与那个地址相关,在RAT中存放的是部分物理地址信息,虽然在一个CPU中物理地址可能有40,46或者48位,但是在Cache中并不需要存放全部地址信息。因为从Cache的角度上看,CPU使用的地址被分解成为了若干段,如图2‑3所示。

2.1 <wbr>Cache的工作原理

这个地址也可以理解为CPU访问Cache使用的地址,由多个数据段组成。首先需要说明的是Cache Line Index字段。这一字段与图2‑1中的Cache Line Number类似,CPU使用该字段从Cache中选择一个或者一组Entry[2]

Bank和Byte字段之和确定了单个Cache的Data字段长度,通常也将这个长度称为Cache 行长度,图2‑3所示的微架构中的Cache Block长度为64字节。目前多数支持DDR3 SDRAM的微架构使用的Cache Block长度都是64字节。部分原因是由于DDR3 SDRAM的一次Burst Line为8[24],一次基本Burst操作访问的数据大小为64字节。

在处理器微架构中,将地址为Bank和Byte两个字段出于提高Cache Block访问效率的考虑。Multi-Bank Mechanism是一种常用的提高访问效率的方法,采用这种机制后,CPU访问Cache时,只要不是对同一个Bank进行访问,即可并发执行。Byte字段决定了Cache的端口位宽,在现代微架构中,访问Cache的总线位宽为64位或者为128位。

剩余的字段即为Real Address Tag,这个字段与单个Cache中的Real Address Tag的字段长度相同。CPU使用地址中的Real Address Tag字段与Cache Block的对应字段和一些状态位进行联合比较,判断其访问数据是否在Cache中命中。



[1] 该图源自[19]的Figure 2. A typical cache and TLB design,拷贝后过于模糊,重画这个示意图,并有所改动。

[2] 如果使用Set-Associative方式组织Cache结构,此时使用Index字段可以获得一组Entry。

 

2.2 Cache的组成结构

  (2011-09-19 14:05:38)
标签: 

杂谈

分类: 浅谈CacheMemory

由上文所述,在一个Cache中包含多行多列,存在若干类组成方式。在处理器体系结构的历史上,曾出现过更多的的组成结构,最后剩余下来的是我们耳熟能详的Set-Associative组成结构。这种结构在现代处理器微架构中得到了大规模普及。

在介绍Set-Associative组成结构之前,我们简单回顾另外一种Cache组成结构,Sector Buffer方式[23]。假定在一个微架构中,Cache大小为16KB,使用Sector Buffer方式时,这个16KB被分解为16个1KB大小的Sector,CPU可以同时查找这16个Sector。

CPU访问的数据不在这16个Sector中命中时,将首先进行Sector淘汰操作,在获得一个新的Sector后,将即将需要访问的64B数据填入这个Sector。如果CPU访问的数据命中了某个Sector,但是数据并不包含在Sector时,将相应的数据继续读到这个Sector中。采用这种方法时,Cache的划分粒度较为粗略,对程序的局部性的要求过高。Cache的整体命中率不如采用Set-Associative的组成方式[23]。

在狭义Cache的设计中,这种方法已经不在使用。但是这种方法依然没有完全失效。处理器体系结构是尺有所短,寸有所长。在一些广义Cache的设计中,Sector Buffer方式依然是一种行之有效的Buffer管理策略。有很多程序员仍在不自觉地使用这种方式。这种不自觉的行为有时是危险的,太多不自觉的存在有时会使人忘记了最基础的知识。

我曾经逐行阅读过一些工作了很多年的工程师的Verilog代码,在这些代码中使用了一些算法,这些算法我总感觉似曾相识,却已物是人非。他们采用的算法实际上有许多经典的实现方式,已经没有太多争论,甚至被列入了教科书中。有些工程师却忘记了这些如教科书般的经典,可能甚至没有仔细阅读过这些书籍,在原本较为完美的实现中填入蛇足。更为糟糕的是,一些应该存在的部件被他们轻易的忽略了。

有时间温故这些经典书籍是一件很幸运的事情。我手边常备着A Quantitative Approach和The Art of Computer Programming这些书籍,茶余饭后翻着,总能在这些书籍中得到一些新的体会。时间总是有的。很多人一直在抱怨着工作的忙碌,没有空余,虽然他们从未试图去挤时间的海绵,总是反复做着相同的事情。多次反复最有可能的结果是熟能生巧而为匠,却很难在这样近乎机械的重复中出现灵光一现的机枢。这些机枢可能发生在你多读了几行经文,或受其他领域的间接影响,也许并不是能够出现在日常工作的简单重复之上。

写作时回忆Sector Buffer机制时写下了这些文字。在现代处理器中,Cache Block的组成方式大多都采用了Set-Associative方式。与Set-Associative方式相关的Cache Block组成方式还有Direct Mapped和Fully-Associative两种机制。Direct Mapped和Fully-Associative也可以被认为是Set-Associative方式的两种特例。

在上世纪90年代,Direct Mapped机制大行其道,在Alpha 21064,21064A和21164处理器中,L1 I Cache和D Cache都使用了Direct Mapped方式和Write Through策略。直到Alpha 21264,在L1 Cache层面才开始使用2-Way Associative方式和Write Back策略[16][17][18]。即便如此Alpha 21264在L1 I Cache仍然做出了一些独特设计,采用了2-Way Set-Predict结构,在某种程度上看使用这种方式,L1 I Cache相当于工作在Direct Mapped方式中。

90年代,世界上没有任何一个微架构能够与Alpha相提并论,没有任何一个公司有DEC在处理器微架构中的地位,来自DEC的结论几乎即为真理,而且这些结论都有非常深入的理论作为基础。与Fully-Associative和Set-Associative相比,Direct Mapped方式所需硬件资源非常有限,每一次存储器访问都固定到了一个指定的Cache Block。这种简单明了带来了一系列优点,最大的优点是在200~300MHz CPU主频的情况下,Load-Use Latency可以是1个Cycle。天下武功无坚不破,唯快不破。

至今很少有微架构在L1层面继续使用Direct Mapped方式,但是这种实现方式并没有如大家想象中糟糕。围绕着Direct Mapped方式,学术界做出了许多努力,其中带来最大影响的是Normal P. Jouppi书写的“Improving Direct-Mapped Cache Performance by Addition of a Small Fully-Associative Cache and Prefetch Buffers”,其中提到的Victim Cache,Stream Buffer[20]至今依然在活跃。

使Direct Mapped方式逐步退出历史舞台的部分原因是CPU Core主频的增加使得Direct Mapped方式所带来的Load-Use Latency在相对缩小,更为重要的是呈平方级别增加的主存储器容量使得Cache容量相对在缩小。

Cache Miss Rate的层面考虑,一个采用Direct Mapped方式容量为N的Cache其Miss Rate与采用2-Way Set-Associative方式容量为N/2的Cache几乎相同。这个Observation被John和David称为2:1 Cache Rule of Thumb[7]。这意味着采用Direct Mapped方式的Cache,所需要的Cache容量相对较大。

近些年L1 Cache与主存储器容量间的比值不但没有缩小而是越来越大。L1 Cache的大小已经很少发生质的变化了,从Pentium的16/32KB L1 Cache到Sandy Bridge的64KB L1 Cache,Intel用了足足二十多年的时间。在这二十多年中,主存储器容量何止扩大了两千倍。相同的故事也发生在L2与L3 Cache中。这使得在采用Direct Mapped方式时,Cache的Miss Ratio逐步提高,也使得N-Way Set-Associative方式闪亮登场。

采用Set-Associative方式时,Cache被分解为S个Sets,其中每一个Set中有N个Ways。根据N的不同,Cache可以分为Fully-Associative, N-Ways Set-Associative或者是Direct Mapped。在Cache的总容量不变的情况下,即S×N的值为一个常数M时,N越大,则S越小,反之亦然。8-Way Set-Associative Cache的组成结构如图2‑4所示。

2.2 <wbr>Cache的组成结构

如图2‑4所示,在Cache Block中Real Address Tag字段与数据字段分离,因为这两类字段分别存在不同类型的存储器中。在同一个Set中,Real Address Tag阵列多使用CAM(Content Addressable Memory)存放,以利于并行查找PS(Parallel Search)方式的实现,在设计中也可以根据需要使用串行查找方式SS(Sequential Search)。与PS方式相比,SS方式使用的物理资源较少,但是当使用的Ways数较大时,采用这个方式的查找速度较慢。在现代微架构中,数据字段组成的阵列一般使用多端口,多Bank的SRAM。下文主要讨论PS的实现方式。

对于一个Cache,其总大小由存放Tag阵列和SRAM阵列组成,在参数N较低时也可以采用RAM Tag,本篇仅讨论CAM Tag,在功耗日益敏感的今天,Highly Associative Cache倾向于使用CAM Tag。在许多微架构中,如Nehalem微架构的L1 Cache为32KB[12],这是的大小是指SRAM阵列,没有包括Tag阵列。Tag阵列占用的Die Size不容忽略。

在说明Set-Associative方式和Tag阵列之前,我们进一步讨论NS这个参数。不同的处理器采用了不同的Cache映射方式,如Fully-Associative, N-Ways Set-Associative或者是Direct Mapped。如果使用NS参数进行描述,这三类方式本质上都是N-Ways Set-Associative方式,只是选用了不同的NS参数而已。

在使用N-Ways Set-Associative方式时,Cache首先被分解为多个Set。当S参数等于1时,即所有Cache Block使用一个Set进行管理时,这种方式即为Fully-Associative;N参数为1时的管理方式为Direct Mapped;当NS参数不为1时,使用的方式为N-Ways Set-Associative方式。在图2‑4中,N为8,这种方式被称为8-Way Set-Associative。

诸多研究结果表明,随着N的不断增大,Cache的Miss Ratio在逐步降低[7][23]。这并不意味着设计者可以使用更大的参数N。在很多情况下,使用更大的参数N并不会显著降低Miss Ratio,就单级Cache而言,8-Way Set-Associative与Fully-Associative方式从Miss Ratio层面上看效果相当[7]。许多微架构使用的16-Way或者更高的32-Way Set-Associative,并不是单纯为了降低Miss Ratio。

随着Way数的增加,即便Cache所使用的Data阵列保持不变,Cache使用的总资源以及Tag比较所需要的时间也在逐步增加。在一个实际的微架构中,貌似巨大无比的CPU Die放不下几片Cache,在实际设计中一个Die所提供的容量已经被利用的无以复加,很多貌似优秀的设计被不得已割舍。这也使得在诸多条件制约之下,参数N并不是越大越好。这个参数的选择是资源权衡的结果。

S参数为1时,N等于M,此时Tag阵列的总Entry数目依然为M,但是对于CAM这样的存储器,单独的一个M大小的数据单元所耗费的Die资源,略微超过S个N大小的数据单元。随着N的提高,Tag阵列所消耗的比较时间将逐步增加,所需要的功耗也在逐级增大。在进一步讨论这些细节知识前,我们需要了解CAM的基本组成结构,如图2‑5所示。

2.2 <wbr>Cache的组成结构

2‑5中所示的CAM中有3个Word,每一个Word由4个Bits组成,其中每一个Bit对应一个CAM Cell。其中每一个Word对应一条横向的ML(Match Line),由ML0~2组成。在一个CAM内,所有Word的所有Bits将同时进行查找。在一列中,Bits分别与两个SL(Serach Line)对应,包括SL0~3~SL0~3#。其中一个Bit对应一个CAM Cell。

使用CAM进行查找时,需要首先将Search Word放入Search Data Register/Drivers中,之后这个Search Word分解为若干个Bits,通过SL或者~SL发送到所有CAM Cell中。其中每一个CAM Cell将Hit/Miss信息传递给各自的ML。所有ML信息将最后统一在一起,得出最后的Hit/Miss结论,同时也将给出在CAM的哪个地址命中的信息[1]。由此可以发现,由于CAM使用并行查找方式,其查找效率明显操作SRAM。

这也使得CAM得到了广泛应用。但是我们依然无法从这些描述中,获得随着CAM包含的Word数目增加,比较时间将逐步增加,所需要的功耗也在逐级增大这个结论,也因此无法解释Cache设计中为什么不能使用过多的Way。为此我们需要进一步去微观地了解CAM Cell的结构和Hit/Miss识别机制。在门级电路的实现中,一个CAM Cell的设计可以使用两种方式,一个是基于NOR,另一个是基于NAND,如图2‑6所示。

2.2 <wbr>Cache的组成结构

在上图中可以发现NOR-Type CAM Cell和NAND-Type CACM Cell分别由4和3个MOSGET加上一个SRAM Cell组成,一个SRAM Cell通常由6个Transistor组成[2]。由此可以发现一个基本的NOR-Type CAM Cell由10个,NAND-Type CACM Cell由9个Transistor组成。

这不是NOR/NAND CAM Cell的细节,NOR CAM Cell有9个Transistor的实现方式,NAND CAM Cell有10个Transistor的实现方式,这些由实现过程中的取舍决定。NOR和NAND CAM的主要区别是Word的查找策略,NOR CAM Cell采用并行查找方式,NAND CAM Cell使用级联方式进行查找,如图2‑7所示。

2.2 <wbr>Cache的组成结构

为节约篇幅,我省略如何对这些晶体管进行Precharge,Discharge,如何Evaluate,并最终确定一个Word是Hit还是Miss,读者可以进一步阅读[25]获得详细信息。较为重要的内容是从图2‑7中可以发现,当使用NAND CAM时,Word的匹配使用级联方式,MLn需要得到MLn-1的信息后才能继续进行,采用NOR CAM是全并行查找。仅从这些描述上似乎可以发现,NOR CAM在没有明显提高Transistor数目的前提之下,从本质上提高了匹配效率,貌似NAND CAM并没有太多优点。

事实并非如此。在使用NOR CAM时,随着Word包含Bit数目的增加,ML上的负载数也随之增加。这些过多输入的与操作将带来延时和功耗。在图2‑7所示的N-Input与操作采用了One-Stage实现方式,需要使用Sense Amplifier降低延时,但是这个Sense Amplifier无论在工作或者处于Idle状态时的功耗都较大。当然在n较大时,Multi-Stage n-input AND Gate所产生的延时更不能接受。

即便使用了Sense Amplifier,也并不能解决全部问题。当时钟频率较高时,这种并联方式所产生的延时很难满足系统需求。使用NAND CAM方式是一推一关系,没有这种负载要求。在Cache的Tag中除了需要保存Real Address之外还有许多状态信息,这也加大了AND Gate的负担。

CAM除了横向判断一个Word是Hit还是Miss之外,还有另外一个重要问题,就是Search Data Register/Driver的驱动能力问题,这个问题进一步引申就是N-Way Set-Associative中N究竟多大才合理的问题。一个门能够驱动多少个负载和许多因素相关,频率,电流强度和连接介质等。对于一个运行在3.3GHz的L1 Cache而言,一个Cycle仅有300ps,在这么短的延时内,门级电路做不了太多事情。

我们可能有很多方法提高驱动能力,Buffering the fan-out,使用金属线介质或者更多级的驱动器,缩短走线距离等等。在所有追求极限的设计中,这些方法不是带来了过高的延时,就是提高了功耗。一个简单的方式是采用流水方式,使用更多的节拍,但是你可知我为了节省这一拍延时经历了多少努力。这一切使得在N-Ways Set-Associative方式中N的选择是一个痛苦的折中,也使得在现代微架构的L1 Cache中N不会太大。

尽管有这些困难,绝大多数现代微架构还是选择了N-Ways Set-Associative方式,只是对参数N进行了折中。在这种方式下,当CPU使用地址r(i)进行存储器访问时,首先使用函数f寻找合适的Set,即s(i) = f(r(i)),然后在将访问的地址的高字段与选中Set的Real Address Tag阵列进行联合比较,如果在Tag阵列中没有命中,表示Cache Miss;如果命中则进一步检查Cache Block状态信息,并将数据最终读出或者写入。

现代微架构多使用2-Ways,4-Ways,8-Ways或者16-Ways Set-Associative方式组成Cache的基本结构。Ways的数目多为2的幂,采用这种组成方式便于硬件实现。然而依然有例外存在,在有些处理器中可能出现10-Ways或者其他非2幂的Ways。

出现这种现象的主要原因是这个Way并非对等。在一个处理器系统中,微架构和外部设备,如显卡和各类PCIe设备,都可以进行存储器访问。这些存储器访问并不类同。在多数情况下,微架构经由LSQ,FLC,MLCs,之后通过LLC,最终与主存储器进行数据交换。外部设备进行DMA访问时,直接面对的是LLC和主存储器,并对L1 Cache和MLC产生简介影响。微架构与外部设备访问主存储器存在的差异,决定了在有些处理器中,LLC Way的构成并不一定是2的幂,而是由若干2的幂之和组成,如10-Ways Set-Associative可能是由一个8-Ways和一个2-Ways组成。

无论是软件还是硬件设计师都欣赏同构的规整,和由此带来的便利与精彩。但是在更多情况下,事物存在的差异性,使得严格的同构并不能发挥最大的功效,更多时候需要使用异构使同构最终成为可能。

Cache中,不对等Way的产生除了因为访问路线并不一致之外,还有一个原因是为了降低Cache Miss Rate,有些微架构进行Way选择使用了不同的算法。如Skewed-Associative Cache[26]可以使用不用的Hash算法,f0f1分别映射一个Set内的两个Way,采用这种方法在没有增加Set的Ways数目的情况下,有效降低了Cache Miss Rate。[26]的结论是在Cache总大小相同时,2-Way Skewed- Associative Cache的Hit Ratio与4-Way Associative Cache相当,其Hit Time与Direct Mapped方式接近。但是在Cache容量较大时,f0f1的映射成本也随之加大,从而在一定程度上增加了Cache的访问时间。

在历史上还出现过其他的Cache组成结构,如Hash-rehash Cache,Column-Associative Cache等,本篇对此不再一一介绍。值得留意的是Parallel Multicolumn Cache[27],这种Cache的实现要点是综合了Direct Mapped Cache和N-Ways Set-Associative方式,在访问Cache时首先使用Direct Mapped策略以获得最短的检索时间,在DM方式没有命中后,再访问N-Ways方式组成的模块。

上述这些方式基本是围绕着Direct Mapped进行优化,目前鲜有现代微架构继续使用这些方法,但是在一个广义Cache的设计中,如果仅存在一级Cache,这些方法依然有广泛活跃着。这也是本节在此提及这些算法的主要原因。

在体系结构领域,针对Cache的Way的处理上,还有很多算法,但是在结合整个Cache层次结构的复杂性之后,具有实用价值的算法并不多。在目前已实现的微架构中,使用的最多的方式依然是N-Ways Set-Associative。

在某些场景下,Miss Penalty无法忍受,比如TLB Miss,无论是采用纯软件还是Hardware Assistance的方法,Miss Penalty的代价都过于昂贵。这使得架构师最终选择了Fully Associative实现方式。在现代微架构中,TLB的设计需要对Hit Time和Miss Rate的设计进行折中,TLB因此分为两级,L1和L2。L1-TLB的实现侧重于Hit Time参数,较小一些,多使用Fully Associative方式,对其的要求是Extremely Fast;L2 TLB的实现需要进一步考虑Miss Rate,通常较大一些,多使用N-Way Associative方式。



[1] 在Cache的设计中,地址信息并不是必要的。在Routing Table的查找中多使用了地址信息。

[2] SRAM Cell还可以使用8个Transistors的实现方式。

 

2.3 Why Index-Aware

  (2011-09-19 14:07:03)
标签: 

杂谈

分类: 浅谈CacheMemory

N-Ways Set-Associative方式的Cache中,CPU如何选用函数f映射Cache中的Set是一个值得讨论的话题。其中最常用的算法是Bit Selection。如图2‑3所示,CPU使用Bits 12~6选择一个合适的Set。此时f(r(i)) = Bits 12~6。

这是一种最快,最简洁的实现方式,使用这种方法带来的最大质疑莫过于Set的选择不够随机。历史上曾经有人试图使用某些pseudo-random算法作为函数f,但是需要明确的是在Set Selection中,严格意义上的Random算法并不可取。

一是因为在Silicon Design中,很难在较短时间内产生一个随机数,即便使用最常用的LFSR(Linear Feedback Shift Register)机制也至少需要一拍的延时,而且也并不是真正随机的。二是因为多数程序具有Spatial Locality特性,依然在有规律地使用Cache,采用严格意义的Random很容易破坏这种规律性。

在许多实现中,Set Selection时选用的pseudo-random算法等效于Hash算法,这些Hash算法多基于XOR-Mapping机制,需要几个XOR门级电路即可实现。诸多研究表明[23][28][29],这种算法在处理Cache Conflict Miss时优于Bit Selection。

在已知的实现中,追求Hit Time的L1 Cache很少使用这类Hash机制,但是这些方法依然出现在一些处理器的MLC和LLC设计中,特别是在容量较大的LLC层面。在MLC层面上,多数微架构使用的Set Selection的实现依然是简单而且有效的Bit Selection方式。

Bit Selection方式所带来的最大问题是在选择Set时,经常发生碰撞。这种碰撞降低了Cache的整体利用率,这使得系统软件层面在使用物理内存时需要关注这个碰撞,也因此产生了与物理内存分配相关的一系列Index-Aware算法。

操作系统多使用分页机制管理物理内存。使用这种机制时,物理内存被分为若干个4KB[1]大小的页面。当应用程序需要使用内存时,操作系统将从空闲内存池中选用一个未用物理页面(Available Page Frame)。选取未用物理页面的过程因操作系统而异。

有些操作系统在选用这个物理页面时,并没有采用特别的算法,对此无所作为。在很多时候,这种无为而治反而会带来某种随机性,无心插柳柳成荫。对于Cache使用而言,这种无所作为很难带来哪怕是相对的随机。

精心编制的程序与随机性本是水火难容,而且这些程序一直在努力追求着时间局部性和空间局部性,最大化地利用着Cache。这使一系列Index-Aware类的Memory分配算法得以引入。在介绍这些Memory分配算法之前,我们首先介绍采用分页机制后,一个进程如何访问Cache,其示意如图2‑8所示。

2.3 <wbr>Why <wbr>Index-Aware

在多数情况下,操作系统以4KB为单位将Memory分解为多个页面。如上图所示,这个4KB的页边界将Cache Line Index分解成两个部分,其中在Page Frame中的部分被称为Bin Index,在Page Off中的部分被本篇称为Offset Index。以此进行分析,Memory分配算法也被分为两大类,一类是Bin Index Aware,另一类是Offset Index Aware Memory分配算法。

我们首先简要介绍Bin Index的优化。根据Bin Index的不同,Cache被分为Large Cache和Small Cache两类。当一个Cache的容量除以Way数大于实际使用的物理页面时,这种Cache被称为Large Cache,反之被称为Small Cache。

在许多处理器系统的实现中,L2和L3一般都属于Large Cache,L1 Cache需要视情况而定。如Sandy Bridge L1 Data Cache的大小为32KB,8-Ways结构[5],两者之商为4KB,不大于4KB页面,该Cache即为Small Cache。例如Opteron的L1 Data Cache为64KB,2-Ways结构[6],两者之商为32KB,大于4KB页面,该Cache即为Large Cache。

需要注意的是4KB只是在多数操作系统中常用的物理页面大小,有些操作系统可以使用更大的页面,如8KB,16KB等。这使得Small Cache和Big Cache的划分不仅与微架构相关,而且与操作系统的具体实现相关。一个物理页面大小的使用与许多因素相关,页面越大,碎片问题也越发严重,但是与此相关的TLB Miss Rate也越低。在上文提到的,在Superpages中存在的Allocation,Relocation,Promotion,Pollution和Fragmentation Control等若干问题,随着页面大小的增加,其解决难度也在逐步增大。

另外需要注意的是,这里的“物理页面”指实际使用的物理页面。在1GB大小的Superpage面前,所有Cache都应该属于Small Cache,但是只有极少有应用真正这样使用这个1GB页面,在多数情况下,1GB大小的Superpage仍然被分解为更小的物理页面,可能是4KB,8KB,或者是其他尺寸。

Bin-Index优化算法的实现要点是,不同的物理页面在使用Cache时,尽量均匀分配到不同的Bin Box中。如图2‑8所示,在一个Bin中通常有若干个Set,Set中还有若干个数据单元,当所有Set的所有数据单元都被占用时,如果有新的物理页面依然要使用这个Bin Box时,将在一定程度上引发Cache Contention,即便在Cache中仍有其他未用的Block。

对于一个进程而言,产生Cache Contention原因是,一个进程使用的虚拟地址空间虽然连续,但是在进行虚实映射时,内存分配器如果没有进行Bin-Index的优化手段,将“随机”选取一个物理页面与之对应,这个“随机”不但不是“随机”的,而且有非常强的规律性,从而造成了一些原本不该出现的Cache Contention。

[30]列举了几个常用的Bin-Index的优化算法,如Page Coloring,Bin Hooping,Best Bin和Hierarchical。有些算法已经在FreeBSD和Solaris中得到了实现,在Linux系统中,目前尚未使用这些Bin-Index算法。

但是我们不能得出因为Linux系统没有使用这类算法而导致性能低下这一结论。上文所述的所有Bin-Index算法都有其优点和缺点。而且从某种意义上说,不使用Bin-Index也是一种Bin-Index算法,同时在许多微架构的Cache设计中,由于Virtual Cache和Hash算法的使用,使得Bin-Index算法并不会取得很好的效果。

Page Coloring算法最为简单,其主要原理是利用了Virtual Cache无需染色的原理,因为在多数情况下地址连续的Virtual Page很少在Cache中冲突,此时在分配物理页面时,其部分低位可以直接使用虚拟地址的低位,从而在一定程度上避免了Cache Contention。如果进一步考虑多进程情况,使用这种算法时还可以将之前的结果与进程的PID参数再次进行XOR-Mapping操作;进一步考虑多内核情况,可以将再之前的结果与内核的Logical Processor ID进行XOR-Mapping操作。

对于使用了Virtual Cache的微架构,并不意味着Bin-Index算法不再适用。因为在多数情况下,Virtual Cache仅在需要进一步降低Hit Time的L1 Cache中使用[2],在L2或者更高层的Cache中,很少再使用这种技术。

[30]中出现的Bin Hooping,Best Bin和Hierarchical算法也并不复杂,这些算法都是利用Temporal Locality原则,其详细实现参见[30],这些算法并不复杂,没有逐行翻译的必要。在微架构和操作系统层面的设计与实现中,使用的多数算法都不复杂。

在一个操作系统实现中,Memory Allocator是一个非常重要的组成部件,其设计异常复杂。有一些人在努力寻求最优的,通用的分配管理原则,虽然最优和通用几乎很难划等号。通用原则有通用原则的适用场合,专用的定制原则也有其存在的必要,没有一个绝对的准则。通常在一个Memory Allocator的设计中需要关注自身的运行效率,如Footprint,False Sharing,Alignment和TLB的优化等一系列问题,也包含Cache-Index的优化问题。

许多操作系统实现了Cache-Index的优化,包括Bin-Index和Offset-Index。一些操作系统统筹考虑Bin和Offset的Index,并将其合并为Cache-Index,其中最著名算法的是Hoard和Slab。近些年也出现了一些较新的Cache-Index Aware算法,如CLFMalloc和CIF(Cache-Index Friendly)[31]。

这些算法都在关注如何寻求合适的策略以避免Cache Index的冲突,尽量使物理页面映射到Cache的不同Set中。需要读者进一步考虑的是,即便为某个应用找到了这些最优的映射方式,这些方式是否能在更广阔的应用领域发挥更大的效率。所有这些最优可能都有其合适的场景,很难有放之四海而皆准的策略。

为此我们首先简要回顾Cache自身的编码方式。下文以一个2-Way Set-Associative,Cache Block长度为64B,总大小为64KB的Cache说明其内部的编码和组成方式。在采用这种方式时,该Cache的Set数目为512。

这种结构的Cache与Opteron的L1 Data Cache类似,如图2‑9所示。由上文所述,Cache由两部分组成,一个是Tag阵列,另一个是数据阵列。在Opteron的L1 Cache中,Tag阵列由512个Set组成,每个Set的Entry数目为2,数据阵列与此一一对应。

2.3 <wbr>Why <wbr>Index-Aware

我们首先讨论Cache的Data阵列。在现代处理器微架构中,从逻辑上看Data阵列首先被划分为多个Set,在每一个Set中含有多个Way,而且每一个Way由多个Bank组成。但是从物理实现上看,Data阵列本质上是一块SRAM,使用连续的物理地址统一编码。从Silicon Design的角度上看,Cache的Data阵列具有地址,其编码方式如图2‑10所示。

2.3 <wbr>Why <wbr>Index-Aware

对于64KB大小的Cache,一共需要16位地址进行编码。为简化起见,我们忽略这个地址的最后6位,仅讨论Set Number和Way Number。一个Cache的物理地址整体连续,然后被Way Number划分成为多个物理地址连续的子块。

CPU访问Cache时,首先使用Set Number访问子块,在2-Ways Set- Associative结构中,将有两个子块被同时选中,之后这些数据将同时进入一个或者Way-Select部件。在这种情况下这个Way-Select部件的数据通路为2选1结构。

在现代处理器中,为提高Cache的数据带宽,通常设置多个Way-Select部件,以组成Multi-Port Cache结构。相应的,AGU也必须具备产生多套地址的能力,分别抵达不同的Tag阵列。从而在微架构中需要设置多个AGU,并为Cache设置多个Tag阵列以支持Multi-Ports Cache结构。多端口Cache的实现代价较大,多数处理器仅在L1层面实现多端口,其他层面依然使用着Single-Port结构。

2‑9中所示的Cache结构使用了2-Ports结构,每多使用一个Port就需要额外复制一个Tag阵列,也由此带来了不小的同步开销,而且过多的Port更易产生Bank Conflict。在现代微架构的实现中,L1 Cache层面多使用2个Port,很少更多的Port。同时为了支持Cache的流水操作,Opteron微架构设置了3个AGU部件,个数超过了2个Port所需要的。其中更深层次的原因是x86处理器EA的计算在某些情况下过于复杂。

Cache的组成结构中,还有一个细节需要额外关注。当CPU对一个物理地址进行访问没有在Cache中命中时,通常意味着一个Cache Block的替换。Cache Block的替换算法对Cache Hit乃至整个Cache的设计至关重要。



[1] 在多数操作系统中,4KB是最常用的页面大小。本篇以此为例说明Index-Aware算法的必要性。

[2] TLB Translation处于L1 Cache访问的Critical Path中,Virtual Cache可以提高转换效率,但依然是一个权衡。

 

2.4 Cache Block的替换算法1

  (2011-09-20 15:06:46)
标签: 

杂谈

分类: 浅谈CacheMemory

在处理器系统处于正常的运行状态时,各级Cache处于饱和状态。由于Cache的容量远小于主存储器,Cache Miss时有发生。一次Cache Miss不仅意味着处理器需要从主存储器中获取数据,而且需要将Cache的某一个Block替换出去。

不同的微架构使用了不同的Cache Block替换算法,本篇仅关注采用Set-Associative方式的Cache Block替换算法。在讲述这些替换算法之前,需要了解Cache Block的状态。如图2‑4所示,在Tag阵列中,除了具有地址信息之外,还含有Cache Block的状态信息。不同的Cache一致性策略使用的Cache状态信息并不相同,如Illinois Protocol[32]协议使用的相关的状态信息,该协议也被称为MESI协议。

MESI协议中,一个Cache Block通常含有MESI这四个状态位,如果考虑多级Cache层次结构的存在,MESI这些状态位的表现形式更为复杂一些。在有些微架构中,Cache Block中还含有一个L(Lock)位,当该位有效时,该Block不得被替换。L位的存在,可以方便地将微架构中的Cache模拟成为SRAM,供用户定制使用。使用这种方式需要慎重。

在很多情况下,定制使用后的Cache优化结果可能不如CPU自身的管理机制。有时一种优化手段可能会在局部中发挥巨大作用,可是应用到全局后有时不但不会加分,反而带来了相当大的系统惩罚。这并不是这些优化手段的问题,只是使用者需要知道更高层面的权衡与取舍。不谋万世者,不足谋一时;不谋全局者,不足谋一域。

Cache Block中,除了有MESIL这些状态位之外,还有一些特殊的状态位,这些状态位与Cache Block的更换策略相关。微架构进行Cache Block更换时需要根据这些状态位判断在同一个Set中Cache Block的使用情况,之后选择合适的算法进行Cache Block更换。常用的Replacement算法有MRU(Most Recently Used),FIFO,RR(Round Robin),Random,LRU(Least Recently Used)和PLRU(Pseudo LRU)算法。

所有页面替换算法与Belady's Algorithm算法相比都不是最优的。Belady算法可以对将来进行无限制的预测,并以此决定替换未来最长时间内不使用的数据。这种理想情况被称作最优算法,Belady's Algorithm算法只有理论意义,因为精确预测一个Cache Block在处理器系统中未来的存活时间没有实际的可操作性,这种算法并没有实用价值。这个算法是为不完美的缓存算法树立一个完美标准。

在以上可实现的不完美算法中,RR,FIFO和Random并没有考虑Cache Block使用的历史信息。而Temporal和Spatial Locality需要依赖这些历史信息,这使得某些微架构没有选用这些算法,而使用LRU类算法,这不意味着RR,FIFO和Random没有优点。

理论和Benchmark结果[23][35][37]多次验证,在Miss Ratio的考核中,LRU类优于MRU,FIFO和RR类算法。这也并不意味着LRU是实现中较优的Cache替换算法。事实上,在很多场景下LRU算法的表现非常糟糕。考虑4-WaySet-Associative方式的Cache,在一个连续访问序列{a, b, c, d, e}命中到同一个Set时,Cache Miss Ratio非常高。

在这种场景下,LRU并不比RR,FIFO算法强出多少,甚至会明显弱于Random实现方式。事实上我们总能找到某个特定的场景证明LRU弱于RR,FIFO和上文中提及的任何一种简单的算法。我们也可以很容易地找到优于LRU实现的页面替换算法,诸如2Q,LRFU,LRU-K,Clock和Clock-Pro算法等。

这些算法在分布存储,Web应用与文件系统中得到了广泛的应用。Cache与主存储器的访问差异,低于主存储器与外部存储器的访问差异。这使得针对主存储器的页面替换算法有更多的回旋空间,使得在狭义Cache中得到广泛应用的LRU/PLRU算法失去了用武之地。PLRU算法实现相对较为简单这个优点,在这些领域体现得并不明显,不足以掩饰其劣势。

LRU算法并没有利用访问次数这个重要信息,在处理File Scanning这种Weak Locality时力不从心。而且在循环访问比广义Cache稍大一些的数据对象时,Miss Rate较高[42]。LRU算法有几种派生实现方式,如LRFU和LRU-K。

LRFU算法是LRU和LFU(Least Frequently Used),LFU算法的实现要点是优先替换访问次数最少的数据。LRU-K算法[38]记录页面的访问次数,K为最大值。首先从访问次数为1的页面中根据LRU算法进行替换操作,没有访问次数为1的页面则继续查找为2的页面直到K,当K等于1时,该算法与LRU等效,在实现中LRU-2算法较为常用。

LRU-K算法使用多个Priority Queue,算法复杂度为O(log2N) [39],而LRU,FIFO这类算法复杂度为O(1),采用这种算法时的Overhead略大,多个Queue使用的空间相互独立,浪费的空间较多。2Q算法[39]的设计初衷是在保持LRU-2效果不变的前提下减少Overhead并合理地使用空间。2Q算法有两种实现方式,Simplified 2Q和Full Version。

Simplified 2Q使用了A1和Am两个队列,其中A1使用FIFO算法,Am使用LRU算法进行替换操作。A1负责管理Cold数据,Am负责管理Hot数据,其中在A1的数据可以升级到Am,但是不能进行反向操作。

如果访问的数据p在Am中命中时将其放回Rear[1];如果在A1中命中,将其移除并放入到Am中。如果p没有在A1或者Am中命中时,率先使用这些Queue中的空余空间,将其放入到A1的Rear;如果没有空余空间,则检查A1的容量是否超过Threshold参数,超过则从A1的Front移除旧数据,将p放入A1的Rear;如果没有超过则从Am的Front移除旧数据,将p放入A1的Rear。

在这种实现中,合理设置Threshold参数至关重要。这个参数过小还是过大,都无法合理平衡A1和Am的负载。这个参数在Access Pattern发生变化时很难确定,很难合理地使用这些空间。LRU-2算法存在同样的问题。这是Full Version 2Q算法要解决的问题。

Full Version 2Q将A1分解为A1in和A1out两个Queue,其中Kin为A1in的阈值,Kout为Aout的阈值。此外在A1in和A1out中不再保存数据,而是数据指针,使用这种方法Am可以使用所有Slot,在一定程度上解决了Adaptive的问题。

如果访问的数据x在Am中命中时将其放回Rear;如果在A1out命中,则需要为x申请数据缓冲,即reclaimfor(x),之后将x放入Am的Rear;如果x在A1in中命中,不需要做任何操作;如果x没有在任何queue中命中,则reclaimfor(x)并将其放入A1in的Rear。

如果A1in,A1out   或者Am具有空闲Slot,reclaimfor(x)优先使用这个Slot,否则在Am,Ain和Aout中查询。如果|Ain|大于Kin时,则首先从Ain的Front处移除identifier y,然后判断|Aout|是否大于Kout,如果大于则淘汰Aout的Front以容纳y,否则直接容纳y。如果Ain和Aout没有超过阈值,则淘汰Am的Front。

这些算法都基于LRU算法,而LRU算法通常使用Link List方式实现,在访问命中时,数据从Link List的Front取出后放回Rear,发生Replacement时,淘汰Front数据并将新数据放入Rear。这个过程并不复杂,但是遍历Link List的时间依然不能忽略。

Clock算法可以有效减少这种遍历时间,而后出现的Clock-Pro算法的提出使FIFO类页面替换算法受到了更多的关注。传统的FIFO算法与LRU算法存在共同的缺点就是没有使用访问次数这个信息,不适于处理Weak Locality的Access Pattern。

Second Chance算法对传统的FIFO算法略微进行了修改,在一定程度上可以处理Weak Locality的Access Pattern。Second Chance算法多采用Queue方式实现,为Queue中每一个Entry设置一个Reference Bit。访问命中时,将Reference Bit设置为1。进行页面替换时查找Front指针,如果其Reference Bit为1时,清除该Entry的Reference Bit,并将其放入Rear后继续查找直到某个Entry的Reference Bit为0后进行替换操作,并将新的数据放入Rear并清除Reference Bit。

Clock算法是针对LRU算法开销较大的一种改进方式,在Second Change算法的基础之上提出,属于FIFO类算法。Clock算法不需要从Front移除Entry再添加到Rear的操作,而采用Circular List方式实现,将Second Change使用的Front和Rear合并为Hand指针即可。

这些算法各有优缺点,其存在的目的是为了迎接LIRS(Low Inter-Reference Recency Set)[40]算法的横空出世。从纯算法的角度上看,LIRS根本上解决了LRU算法在File Scanning,Loop-Like Accesses和Accesses with Distinct Frequencies这类Access Pattern面前的不足,较为完美地解决了Weak Locality的数据访问。其算法效率为O(1),其实现依然略为复杂,但在I/O存储领域,略微的计算复杂度在带来的巨大优势面前何足道哉。

LIRS算法中使用了IRR(Inter-Reference Recency)和Recency这两个参数。其中IRR指一个页面最近两次的访问间隔;Recency指页面最近一次访问至当前时间内有多少页面曾经被访问过。在IRR和Recency参数中不包含重复的页面数,因为其他页面的重复对计算当前页面的优先权没有太多影响。IRR和Recency参数的计算示例如图2‑11所示。

2.4 <wbr>Cache <wbr>Block的替换算法1

其中页面1的最近一次访问间隔中,只有三个不重复的页面2,3和4,所以IRR为3;页面1最后一次访问至当前时间内有2个不重复的页面,所以Recency为2。我们考虑一个更加复杂的访问序列,并获得图2‑12所示的IRR和Recency参数。

2.4 <wbr>Cache <wbr>Block的替换算法1

这组访问序列为{A,D,B,C,B,A,D,A,E},最后的结果是各个页面在第10拍时所获得的IRR和Recency参数。以页面D为例,最后一次访问是第7拍,Recency为2;第2~7中有3个不重复的页面,IRR为3。其中IRR参数为Infinite表示在指定的时间间隔之内,没有对该页面进行过两次访问,所以无法计算其IRR参数。LIRS算法首先替换IRR最大的页面,其中Infinite为最大值;当IRR相同时,替换Recency最大的页面。

IRR在一定程度上可以反映页面的访问频率,基于一个页面当前的IRR越大,将来的IRR会更大的思想;Recency参数相当与LRU。在进行替换时,IRR优先于Recency,从而降低了最近一次数据访问的优先级。有些数据虽然是最近访问的却不一定常用,可能在一次访问后很长时间不会再次使用。如果Recency优先于IRR,这些仅用一次的数据停留时间相对较长。

在一个随机访问序列中,并在一个相对较短的时间内精确计算出IRR和Recency参数并不容易。但是我们不需要精确计算IRR和Recency这两个参数。很多时候知道一个结果就已经足够了。在LIRS算法中比较IRR参数时,只要有一个是Infinite,就不需要比较其他结果。假如有多个infinite,比如C和D,此时我们需要进一步比较C和D的Recency,但是我们只需要关心CR>DR这个结果,并不关心C是4还是5。

LIRS算法的实现没有要求精确计算IRR和Recency参数,而是给出了一个基于LIRS Stack的近似结果。LIRS算法根据IRR参数的不同,将页面分为LIR(Low IRR)和HIR(High IRR)两类,并尽量使得LIR页面更多的在Cache中命中,并优先替换在Cache中的HIR页面。

LIRS Stack包含一个LRU Stack,LRU Stack大小固定由Cache决定,存放Cache中的有效页面,在淘汰Cache中的有效页面时使用LRU算法,用以判断Recency的大小;包含一个LIRS Stack S,其中保存Recency不超过RMAX[2]LIR和HIR页面,其中HIR页面可能并不在Cache中,依然使用LRU算法,其长度可变,用于判断IRR的大小;包含一个队列Q维护在Cache中的HIR页面,以加快这类页面的索引速度,在需要Free页面时,首先淘汰这类页面。淘汰操作将会引发一系列连锁反应。我们以图2‑13为例进一步说明。

2.4 <wbr>Cache <wbr>Block的替换算法1

我们仅讨论图2‑13中Access5这种情况,此时Stack S中存放页面{9,7,5,3,4,8},Q中存放{9,7}。Stack S的{9,7,3,4,8}在Cache中,{3,4,8}为LIR页面,{9,7,5}为HIR页面。其中Cache的大小为5,3个存放LIR页面,2个存放HIR页面。

 


[1] 此处对原文进行了修改。[39]中使用Front不是Rear,我习惯从Front移除数据,新数据加入到Rear。

[2] RMAXRecency的最大值。

 

2.4 Cache Block的替换算法2

  (2011-09-20 15:27:43)
标签: 

杂谈

分类: 浅谈CacheMemory

对页面5的访问并没有在Cache中命中,此时需要一个Free页面进行页面替换。LIRS算法首先淘汰在Q中页面7,同时将这页面在S中的状态更改为不在Cache命中;之后页面8从S落到Q中,状态从LIR迁移到HIR,但是这个页面仍在Cache中,需要重新压栈;页面5没有在Cache中命中,但是在S中命中,需要将其移出后重新压栈,状态改变为在Cache中命中。本篇不再介绍LIRS算法的实现细节,对此有兴趣的读者可以参照[40][41]。

而后出现的Clock-Pro算法是LIRS思想在Clock算法中的体现。Clock-Pro和LIRS都为LIRS类算法。其中Clock-Pro算法实现开销更小,适用于操作系统的Virtual Memory Management,并得到了广泛的应用,Linux和NetBSD使用了该算法;LIRS算法适用于I/O存储领域中,mySQL和Apache Derby使用了该算法。LIRS算法较为完美地解决了Weak Access Locality Access Pattern的处理。在LIRS算法出现之后,还有许多页面替换算法,这些后继算法的陆续出现,一次又一次证明了尚未出现更好的算法在这些领域上超越LIRS算法。

LIRS和Clock-Pro算法在这个领域的地位相当于Two-Level Adaptive Branch Prediction在Branch Prediction中的地位。在详细研读[40][42]的细节之后,发现更多的是作者的实践过程。LIRS算法类不是空想而得,是在试错了99条路之后的发现。这是创新的必由之路。

在掌握必要的基础知识后,也许我们最应该做的并不是研读他人的书籍和论文,更多的是去实践。经历了这些艰辛的实践过程,才会有真正的自信。这个自信不是盲从的排他,是能够容纳更多的声音,尽管发出这个声音的人你是如此厌恶。

与微架构设计相比,在操作系统和应用层面可以有更多的资源和更多的时间,使用更优的页面替换算法。虽然在操作系统和应用层面对资源和时间依然敏感,但是在这个层面上使用的再少的资源和再短的时间放到微架构中都是无比巨大。在微架构的设计中,很多在操作系统和应用层面适用的算法是不能考虑的。

假设一个CPU的主频为3.3GHz,在每一个Cycle只有300ps的情况之下,很多在操作系统层面可以使用的优秀算法不会有充足的时间运行。虽然LRU算法在Simplicity和Adaptability上依然有其优势,在微架构的设计中依然没有得到广泛的应用。即便是LRU算法,在Cache的Ways Number较大的情况之下也并不容易快速实现。当Way Number大于4后,LRU算法使用的Link Lists方式所带来的延时是Silicon Design不能考虑的实现方式。更糟糕的是,随着Way Number的增加,LRU算法需要使用更多的状态位。

下文讨论的Cache Block替换算法针对N-Way Set Associative组织方式。在这种情况下,Cache由多个Set组成,存储器访问命中其他Set时,不会影响当前Set的页面更换策略,所谓的替换操作是以Set为单位进行的。为简化起见,假设下文中出现的所有存储器访问都是针对同一个Set,不再考虑访问其他Set的情况。

通常情况,在N-Way Set Associative的Cache中,快速实现Full LRU最多需要N×(N-1)/2个不相互冗余的状态位,理论上的最小值是Floor(LOG2(N!))个状态位[23]。因此当Way Number大于4之后,所需要的状态位不是硬件能够轻易负担的,所需要的计算时间不是微架构能够忍受的。这使得更多的微架构选用了PLRU算法进行Cache Block的Replacement。

LRU算法相比,PLRU算法使用了更少的存储空间,查找需要替换页面的时间也较短,而且从Miss Rate指标的考量上与LRU算法较为类似,在某些特殊场景中甚至会优于LRU算法[36],从而在微架构的设计中得到了大规模的应用。

PLRU算法有两种实现方式,分别为MRU-Based和Tree-Based方式。MRU-Based PLRU的实现方式是为每一个Cache Block设置一个MRU Bit,存储器访问命中时,该位将设置为1,表示当前Cache Block最近进行过访问。当因为Cache Miss而进行Replacement时,将寻找为0的MRU Bit,在将其替换的同时,设置其MRU Bit为1。

采用这种方式需要避免同一个Set所有Cache Block的MRU Bit同时为1而带来的死锁。考虑一个4-Way Set Associative的Cache,当在同一个Set只有一个Cache Block MRU Bit不为1时,如果CPU对这个Cache Block访问并命中时,则将该Cache Block的MRU Bit设置为1,同时将其他所有Cache Block的MRU Bit设置为0,如图2‑14所示。

2.4 <wbr>Cache <wbr>Block的替换算法2

 在上图中,假设MRU[0:3]的初始值为{1, 1, 0, 0},当一次存储器访问命中 Cache Block2时,MRU[0:3]将迁移为{1, 1, 1, 0};下一次访问命中Cache Block3时,MRU[0:3]的第3位置1,为了避免MRU[0:3]所有位都为1而出现死锁,此时其他位反转为0,即MRU[0:3]迁移为{0, 0, 0, 1};再次命中Cache Block1时,将迁移为{0, 1, 0, 1}。

有些量化分析结果[36]认为MRU-Based实现方式在Cache Miss Ratio的比较上,略优于Tree-Based PLRU方式。但是从实现的角度上考虑,使用MRU-Based实现时,每一个Set都需要增加一个额外的Bit。这并不是问题关键,重要的是MRU-Based实现在搜索为第一个为0的MRU Bit时需要较大的开销,也无法避免为了防止MRU Bits死锁而进行反转开销。

本篇所重点讨论的是Tree-Based PLRU实现方式。下文将以一个4-Way Set Associative的Cache说明PLRU的使用方式,使用更多Way的方式可依此类推。在4-Way情况之下,实现PLRU算法需要设置3个状态位B[0~2]字段,分别与4个Way对应;同理在8-Way情况下,需要7个状态位B[0~7];而采用N-Way Set Associative需要N-1个这样的状态位,是一个线性增长。Tree-Based PLRU使用的替换规则如图2‑15所示。

2.4 <wbr>Cache <wbr>Block的替换算法2

 在Cache Set初始化结束后,B0~2位都为0,此时在Set中的Cache Block的状态为Invalid。当处理器访问Cache时,优先替换状态为Invalid的Cache Block。只有在当前Set中,所有Cache Block的状态位都不为Invalid时,Cache控制逻辑才会使用PLRU算法对Cache Block进行替换操作。

Tree-Based PLRU实现中,搜索过程基于Binary Search Tree,在N较大时,其搜索效率明显高于MRU-Based PLRU。在这种实现中,当所有Cache Block的状态不为Invalid时,将首先判断B0的状态,之后决定继续判断B1或者B2。如果B0为0,则继续判断B1的状态,而忽略B2的状态;如果B0为1,则继续判断B2的状态,而忽略B1的状态。

举例说明,如果B0为0而且B1为0时,则淘汰L0;否则淘汰L1。如果B0为1而且B2为0,则淘汰L2;否则淘汰L3。淘汰合适的Cache Block后,B0~B2状态将被更新。值得注意的是,除了发生Cache Allocate导致的Replacement之外,在Cache Hit时,B0~B2的状态同样需要更新。Cache Set替换状态的更新规则如表2-1所示。

 

2-PLRU Bits的更新规则

Current Access

New State of the PLRU Bits

B0

B1

B2

L0

1

1

No Change

L1

1

0

No Change

L2

0

No Change

1

L3

0

No Change

0

 

以上更新规则比较容易记忆。从图2‑15中可以发现在替换L0时,需要B0和B1为0,与此对应的更新规则就是对B0和B1取反,而B2保持不变;同理替换L3时,需要B0和B2为1,与此对应的更新规则就是对B0和B2取反,而B1保持不变。

依照以上规则,我们简单举一个实例说明PLRU算法的替换和状态迁移规则。假设连续三次的存储器访问分别命中了同一个Set的不同Way,如顺序访问Way 3,0和2。其B0~2的状态迁移如图2‑16所示。

2.4 <wbr>Cache <wbr>Block的替换算法2

 由以上访问序列可以发现,当CPU访问Way 3,0和2之后,B0~B2的状态最后将为0b011,此时如果Cache Block需要进行Replacement时,将优先替换Way 1。这个结果与期望相符,对此有兴趣的读者,可以构造出一些其他访问序列,使得最终结果与LRU算法预期不符。这是正常现象,毕竟PLRU算法是Pseudo的。

根据以上说明,我们讨论与Tree-Based PLRU算法的相关的基本原则。由上文的描述可以发现当Cache收到一个存储器访问序列后,Cache Set的替换状态将根据PLRU算法进行状态迁移,我们假设这些存储器访问序列都是针对一个Set,而且存储器访问使用的地址两两不同。这是因为连续地址的访问不会影响到Cache Set的替换状态,二是因为如果访问序列是完全随机的,几乎没有办法讨论Cache的替换算法。满足这一要求,而且最容易构造的序列是,忽略一个地址的Offset字段,Index保持不变,其上地址顺序变化的存储器访问序列。

为了进一步描述Cache Block的替换算法,我们引入Evict(k)和Fill(k)这两个参数,其中参数k指Way Number。Evict指经过多少次存储器访问后才会将Cache Set中未知的数据完全清除,在一个指定的时间,Cache Set中包含的数据是无法确定的,但是经过Evict(k)次存储器访问可以将这些未知数据全部清除。而在经过Fill(k)次存储器访问后,可以确定在Cache Set中存在那些访问数据,Evict和Fill参数的关系如图2‑17所示。

2.4 <wbr>Cache <wbr>Block的替换算法2

Fill(k)和Evict(k)参数的计算需要分多种情况分别进行讨论。我们首先讨论参数Evict(k)。如果在一个Cache Set内的所有Way的状态都为Invalid,这种情况几乎不用讨论,那么连续K次存储器访问,一定可以将该Set内的所有Way Evict。如果所有Way的状态都是Valid,这种情况也较为容易,同样需要K次存储器访问Evict所有Way。

而如果在一个Cache Set内,有些Way为Invalid而有些不是,这种情况略微复杂一些。而且一次存储器访问可以在Cache中Hit也可能Miss,此时Evict和Fill参数的计算方法更为复杂一些。我们使用EvictmFillm表示存储器访问在Cache中Miss的情况,而使用EvicthmFillhm表示其他情况。

我们首先讨论每一次存储器访问都出现Cache Miss的情况,此时如果在Cache Set内具有Invalid的Cache Block,并不会使用PLRU标准替换流程,而直接使用状态为Invalid的Cache Block。此时将一个Set内所有Way Evict所需要的存储器访问次数如公式2‑1所示。

2.4 <wbr>Cache <wbr>Block的替换算法2

k等于8时,Evictm12。这也是在PowerPC E500手册中,Evict大小为32KB的L1 Dcache需要操作48KB内存区域的原因。使用这一方法时需要注意两个实现细节,一个是Interrupts must be disabled,另一个是The 48-Kbyte region chosen is not being used by the system—that is, that snoops do not occur to this region.[43]。

如果进一步考虑在一个存储器访问序列中,在Cache Set中,不但具有Invalid的Cache Block,而且不是每一次存储器访问都会出现Miss操作而引发Replacement操作,因为可能某次访问了出现Cache Hit时,公式2‑1进一步演化为公式2‑2。

2.4 <wbr>Cache <wbr>Block的替换算法2

在这种情况下,对于8-Way Set Associative的Cache,将一个Set所有Way Evict所需要的存储器访问次数为13。在Cache Block替换算法中,MLS(Minimum Life-Span)参数也值得关注,该参数表示在一个Way在Cache Set中的最小生命周期。如果一次存储器操作命中了一个Way,这个Way至少需要MLS次Cache替换操作后,才能够从当前Set中替换出去。对于Tree-Based PLRU算法,MLS的计算如公式 2‑3所示。

2.4 <wbr>Cache <wbr>Block的替换算法2

由以上公式,可以发现对于一个8-Ways Set Associative的Cache,一个最近访问的Block,其生命周期至少为4,即一个刚刚Hit,或者因为Miss而Refill的Cache Block,至少需要4次Cache Block替换操作后才能被Evict。

MLS参数可以帮助分析Cache的Hit Rate,对于一个已知算法的Cache,总可以利用某些规则,极大提高Hit Rate,只是在进行这些优化时,需要注意更高层次的细节。与Cache Block替换算法相关的FillmFillhm参数的计算如公式 2‑4所示。

2.4 <wbr>Cache <wbr>Block的替换算法2

除了PLRU算法之外,文章[37]对FIFO,MRU和LRU进行了详细的理论推导,这些证明过程并不复杂,也谈不上数学意义上的完美,但是通过这篇文章提出的Fill和Evict算法和相关参数,依然可以从Qualitative Research的角度上论证一个替换算法自身的Beautiful,特别是对于一个纯粹的Cache替换算法,在没有考虑多级Cache间的耦合,和较为复杂的多处理器间的Cache一致性等因素时的分析。

如何选用Cache Block的Replacement算法是一个Trade-Off过程,没有什么算法一定是最优的。在Niagara和MIPS微架构的实现中甚至使用了Rand算法,这个算法实现过程非常简单,最自然的想法是使用LFSR近似出一组随机序列,与Cache Set中设置替换状态相比,LFSR使用的资源较少,而且确定需要Replacement的Cache Block的过程非常快速。

 虽然很多评测结果都可以证明Random算法不如PLRU,但是这个算法使用了较小系统资源,而且系统开销较小。利用这些节省下来的资源,微架构可以做其他的优化。因而从更高层次的Trade-Off看,Random算法并不很差。

在体系结构领域很少有放之四海而皆准的真理。PLRU算法之后,也有许多针对其不足的改进和增强,这些想法可能依然是Trade Off。但是不要轻易否定这些结果,在没有较为准确的量化分析结果之前,不能去想当然。想当然与直觉并不等同。人类历史上,许多伟大的革新源自某个人的直觉。这些革新在出现的瞬间甚至会与当时的常识相悖。

在一个技术进入到稳定发展阶段,很难有质的提高。更多时候,在等待着变化,也许是使用习惯的改变,也许是新技术的横空出世。在许多情况下,CS和EE学科的发展进步并不完全依靠自身的螺旋上升发生的质变,更多的时候也许在耐心等待着其他各个领域的水涨船高。Cache替换算法如此,体系结构如此,人类更高层面的进步亦如此。

近期随着MLP的崭露头角,更多的人重新开始关注Cache Block的替换算法,出现了一系列MLP-Aware的替换算法,如LIN(Linear) Policy[44],LIP(LRU Insertion Policy),BIP(Bimodal Insertion Policy)和DIP(Dynamic Insertion Policy)[45]。

其中LIN算法根据将Cache Miss分为Multiple Cache Miss和Isolated Miss。其中Isolated Miss出现的主要场景是Pointer-Chasing Load操作,而Multiple Cache Miss发生在对一个Array的操作中。Multiple Cache Miss可以并行操作,Amortize所有开销,对性能的影响相对较小;而Isolated Miss是一个独立操作对性能影响较大,传统Cache Block的替换算法并没有过多考虑这些因素,从而影响了性能。LIN算法即是为了解决这些问题而引入[44]。

LIP,BIP和DIP针对Memory-Intensive的应用。在某些Memory-Intensive应用中,可能存在某段超出Cache容量的数据区域,而且会按照一定的周期循环使用。如果采用传统的LRU算法,最新访问的Cache Block将为MRU,在超过MLS个Cycle后才可能被替换,而将不应该替换的Cache Block淘汰,从而造成了某种程度的Cache Trashing。

采用LIP算法时,则将这样的Incoming Cache Block设置为LRU,以避免Cache Trashing,这种思想谈不上新颖,重要的是简洁快速的实现。BIP是LIP的改进。DIP是对BIP和传统的LRU算法进行加权处理,以实现Adaptive Insertion Policies[45]。

除了在Memory-Intensive的应用层面之外,相对在不断提高的DDR延时,也改变着Cache Block Replacement算法的设计。现代处理器的设计对不断提高的DDR延时在本质上束手无策,只有引入更多的Cache层次,采用容量更大的LLC,在满足访问延时的同时获得更高的Coverage与不断增长的DDR容量和访问延时匹配。与此对应,产生了一些用于多级Cache结构的Cache Block替换算法,如Bypass and Insertion Algorithms for Exclusive LLC[46]。

这些算法的出现与日益增加的主存储器容量与访问延时相关,也是当前Cache Block替换算法的研究热点。这为Cache Hierarchy的设计提出了新的挑战,使得原本已经非常难以构思,难以设计难以验证的Cache层次结构,愈加复杂。

 
 

2.5 指令Cache

  (2011-09-21 07:22:17)
标签: 

杂谈

分类: 浅谈CacheMemory

在一个处理器系统中,指令Cache与数据Cache的组成方式和使用规则有所不同。在现代处理器系统中,在L1 Cache层面,指令Cache与数据Cache通常分离,而在其后的Cache层次中,指令与数据混合存放,在多数情况下L1指令Cache是只读的,因此Cache Block中包含的状态较少一些,一致性处理相对较为简单。

与指令Cache相比,数据Cache的设计与实现复杂得多。在此回顾指令Cache的主要原因是,在之后的篇章中,我会专注于介绍数据Cache。在目前已知的微架构中,x86体系结构的指令Cache和指令流水线的设计最为复杂。所以本节只介绍x86处理器中指令Cache和与其相关的设计。这些复杂度与x86处理器不断挑战着处理器运行极限直接相关,此外由x86的Backword-Compatibility而继承的变长CISC指令也需要对此负责。这些变长指令对于设计者是一个不小的灾难。在这场灾难中,x86架构久病成良医促成了一个又一个的发现。

通常情况下,工业界很少有大的成果能够领先于学术界。更多的情况是理论上较为成熟的技术在若干年之后被Industry采纳。Intel的伟大之处在于在微架构的很多领域中领先于理论界。更加令人深思的是,Intel的很多发现其起源是为了解决自身的并不完美。

Intel的每一代Tock,几乎都是从Branch Predictor的设计与优化开始,并基于此重新构建指令流水与Cache Hierarchy结构。在微架构中功耗与性能的权衡主要发生的领域也集中于此。在处理器系统中最大的功耗损失莫过于把一些已经执行完毕的指令抛弃和一些不必要重复的操作。在指令执行阶段,Misprediction将刷新指令流水线,将丢弃很多In-Flight的指令,对于x86处理器,丢弃一条最长可达256b的指令,是一个不小的损失。而Cache Hierarchy是微架构耗费资源最多的组成部件。一个处理器将广义和狭义Cache去掉之后,所剩无几。

Intel x86处理器的Tick-Tock的不断运行过程中,Branch Predictor的设计依然在不断发展,指令流水线的设计也随之改变。虽然指令流水线并不是本篇所关注的重点,但仍有必要在此介绍一些与指令Cache直接相关的内容。在x86微架构中,一条指令流水线通常被分为Front-End和Back-End两大部分。在Front-End与Back-End之间使用DQ(Decoder Queue)连接,其结构如图2‑18所示。

2.5 <wbr>指令Cache

Front-End负责指令的Fetches和Decodes,将指令发送给Decoder Queue,相当于Producer;而Back-End在条件允许的情况在从Decoder Queue中获得指令并执行,相当于Consumer。指令在Back-End中执行完毕后,需要将结果反馈给Decoder Queue,进一步处理Depedency和资源冲突,同时还需要为Front-End中的Branch Predictor反馈Branch指令的最终执行结果。Branch Predictor使用这些反馈确认是否发生了Misprediction。

Front-End,Decoder Queue和Back-End之间需要协调工作,以保证整个流水线的顺利运转。理想的情况是Front-End可以将指令源源不断地发送给Decoder Queue,而Back-End可以顺利地从Decoder Queue获得指令,采用这种方式似乎只要不断提高Front End送至Decoder Queue的指令条数(Instruction Block),就可以不断的提高ILP。

但是这个Block数目并不能无限制提高,因为在DQ中的指令流已经被Branch指令切割成为若干个子指令流,物理地址连续的指令流从程序执行的角度上看并不连续,为此现代处理器多设置了强大的Branch Predictor猜测程序即将使用的子块。

但是对于一个指令流水线过长的微架构,一次Misprediction所带来的Penalty仍然足以击溃指令流水的正常运行。基于这些考虑,Intel在Pentium IV处理器中,率先使用了Trace Cache机制以最大限度的使指令流水线获得事实上连续的指令流。

从本质上看,Trace Cache机制是一个机器的自学习加修正策略,试图让机器尽可能的分析本身难以琢磨的指令流,捕获其运行规律,使得指令流水获得的指令尽量连续。从整个微架构的发展历史上看,少有这样级别机器智能的成功案例。为什么偏要赋予仅能识别0和1这两个数字的机器如此重任。从历史的进程上看,从微架构中流传下来更多的是简单精炼的设计。偏执的Pentium IV使用了Trace Cache。

这个后来被John和David称为“likely a one-time innovation”[7]的技术,在当时依然受到热捧,“Trace cache: a low latency approach to high bandwidth instruction fetching”这篇文章被公开索引了五百多次。

当时依然有很多人质疑使用Trace Cache提高的性能与付出的代价严重不成比例,但是有更多的学者在大书特书这个主题,根据这些文章提供的模型和相关的Quantitative Analysis,不难得出Trace Cache是一个伟大发明的结论,直到高频低能的Pentium IV被AMD的Opteron彻底击败。尽信量化分析结果不如没有。

Trace Cache的组成结构和使用方法不在本书的讨论范围内,对此部分有兴趣的读者可以参考[68]获得详细信息。在Intel后继发布的微架构中已不见Trace Cache的踪迹,但是依然不能全盘否定Pentium IV构架使用的指令预取,其详细过程如图2‑19所示。

2.5 <wbr>指令Cache

x86架构中,FLC被分解为指令与数据Cache, MLCs和LLC同时存放这指令与数据。这种方式是当代绝大多数微架构采用的Cache组成方式,也被称为Harvard Architecuture,更为准确的称呼应该是Modified Harvard Architecuture。

Pentium IV架构中,指令将从L2 Cache首先到达Instruction Stream Buffers,为简化起见,本节不讨论所预取的指令在L2 Cache Miss的情况。只要Instruction Stream Buffers不满,预读地址有效,控制逻辑就会将指令从L2 Cache Lookup单元源源不断地传递下去。

如果在Instruction Stream Buffer中的指令为Conditional Branch时,该指令将首先被送至Front-End BTB进行查找,如上文所述,在一个程序执行过程中使用的指令流并不连续而是被Conditional Branch分割成为若干个子块,因此需要对这类指令进行特别的处理。

这些指令将被保存在Front-End BTB中,这个Front-End BTB也是一种广义Cache,其组成结构和使用方式与狭义Cache类似。因为在Front-End BTB中只有4096个Entry,所以Miss时有发生,在Miss时,需要根据相应的替换算法淘汰一个Entry,并重新创建一个Entry,但是并不会改变指令的预读路径,同时Conditional Branch还需要转交给Static Branch Predictor做进一步处理,本节不关心这种情况。

如果Hit,需要检查BTB预测的结果是Not Taken还是Taken。如果是Not Taken,BTB不会通知指令预读单元改变预读路径;如果是Taken,则将BTB预测的Target Address送至指令预取单元,最终传递到L2 Cache Lookup单元,将Target Address指向的指令数据向下传递给Instruction Stream Buffers。

Conditional Branch从指令单元中Retire时,会将Commit的结果传递给Front-End BTB和Trace Buffer BTB。如果Conditional Branch最终的执行结果与BTB的预测结果相同时皆大欢喜。不同时即为Misprediction,会带来一系列严厉的系统惩罚,不再详细描述这个过程。

Instruction Stream Buffers会将CISC指令继续传递给IA μop Decoder。Decoder将绝大部分的CISC指令翻译成为μops并送入Trace Cache,同时按序送入Decoder Queue。此时在Trace Cache中保存的是μops,而不再是原始的CISC指令。部分CISC指令,如会被送入到Microcode ROM,进行查表译码,这些CISC指令较为复杂,通常会被分解为5条以上的μops,设立Microcode ROM是x86架构的无奈,我们忽略这个无奈。     

译码后得到的μops将依次进入Trace Cache。其中Conditional Branch译码后得到的μops需要进行额外处理,因为Trace Cache的实现要点是记录这些Conditional Branch连接而成的指令流而不是物理地址连续的指令流。

Pentium IV为Trace Cache设立了专用的BTB,Trace Cache BTB,并且希望其命中率最好是100%,这样Decoder Queue中的μops可以全部来自Trace Cache中已有的已经完成译码的指令集合。这只是一个理想情况,依然存在着Branch Target没有在这个BTB中命中的情况,此时依然需要回退到Address Translation部件,进行指令预取。Trace Cache BTB依然会监听Conditional Branch最后的执行情况,并做进一步处理。

Pentium IV微架构在Front-End所做的各种努力,并没有改变其高频低能的结局。这一个几乎给Intel带来浩劫的微架构最终被抛弃,以色列IDC团队的Core Architecuture拯救了Intel。Intel启动了Tick-Tock计划,更加优秀的微架构Nehalem横空出世,很快是Sandy Bridge。

Nehalem没有保留Trace Cache,Sandy Bridge微架构也没有。但是Pentium IV的Front-End并没有轻易地被抛弃,我们依然可以在Nehalem和Sandy Bridge微架构中找到源自Pentium IV的设计,其Front-End的组成结构如图2‑20所示。

2.5 <wbr>指令Cache

Nehalem的Front-End源自Core Archtecture,与Pentium IV有较大差异。Pentium IV巨大的失败阴影左右了Intel后续微架构的设计。Pentium IV流水线的多数设计没有被继承,除了一个源自Trace Cache的附带品。x86架构的CISC指令过于冗长,与其他微架构相比译码过程也复杂很多。在Trace Cache中保留经过已经完成译码的μops是一个不错的设想。

Merom微架构继承了这个设想,使用Instruction Loop Buffer保留这些μops,在Nehalem中这个部件被称为μops Loop Buffer,最后Sandy Bridge使用一个1.5KB的μops Cache保留这些已经完成译码的μops[1]。

这个Decoded μops Cache也被Sandy Bridge微架构称之为L0 Cache[1]。Sandy Bridge微架构采用L0指令Cache除了可以保留珍贵的译码结果之外,更多的是基于Power/Energy的考虑。如果在一个程序的执行过程中,检测到Loop后,将直接从μops Cache中获得指令,暂时关闭并不需要的部件以节约功耗。

历经Nehalem和Sandy Bridge两轮Tock之后,x86处理器在Branch Predictor部件的设计中虽然仍有调整,但是日趋稳定。x86微架构引入Two-Level Adaptive Branch Prediction[47]机制之后,BTB的变化更加细微。除了μops Cache之外,Nehalem微架构Branch Predictor使用的Gshare Predictor,Indirect Branch Target Array和Renamed Return Stack Buffer这几个重要部件被Sandy Bridge架构继承[70]。

Pentium IV相比,Core,Nehalem和Sandy Bridge微架构简化了Front-End的设计,也使我失去了进一步使用文字叙述的动力。但是与其他微架构,如Power和MIPS相比,不论是Nehalem还是Sandy Bridge,在Front-End的设计中依然投入了大量的资源。对于x86体系结构,也许在放弃了Backword-compatibility之后,才能真正地改变Front-End的设计。也许到了那一天,Front-End这个专用术语也会随之消失。

 

2.6 Cache Never Block

  (2011-09-21 07:23:46)
标签: 

杂谈

分类: 浅谈CacheMemory

在一个微架构中,有两条值得重点关注的流水线,一个是指令流水线。另一个是Cache Controller使用的流水线,下文将其简称为Cache流水线。这两条流水线的实现对于微架构的性能至关重要。指令流水线的设计与Cache流水线相关,反之亦然。

1967年6月,来自IBM的ROBERT MACRO TOMASULO先生发明了最后以自己名字命令的算法[48],这个算法最终使得Alpha处理器,MIPS处理器,Power处理器,x86处理器,ARM系列处理器,所有采用OOO技术的处理器成为可能。1997年Eckert-Mauchly Award正式授予TOMASULO先生, for the ingenious Tomasulo's algorithm, which enabled out-of-order execution processors to be implemented.

2008年4月3日,TOMASULO先生永远离开我们。他的算法也历经了多轮改进。即便在针对ILP的优化因为Memory Stall而处境艰难,TOMASULO算法也并不过时。虽然本篇文章的重点并不在Superscalar和OOO,仍然建议所有读者务必能够清晰地理解TOMASULO算法和Superscalar指令流水的细节。为节约篇幅,本篇不会对这些知识做进一步的说明。

而在Cache流水线中,Non-Blocking几乎等同于TOMASULO算法在指令流水中的地位。在现代处理器中,几乎所有Cache Hierarchy的设计都采用了Non-Blocking策略。Non-Blocking Cache实现为Superscalar处理器能够进一步发展提供了可能。

假设在一个Superscalar处理器中,一个时钟周期能够发射n条指令。这要求该处理器需要设置多个执行部件,提供充分的并行性。假设在这个处理器中,某类功能部件x所能提供的带宽为BWX,此处的带宽是借用存储器的概念,如果该功能部件需要4拍才能完成一次操作,那么该功能部件为指令流水线所提供的带宽为1/4。

在一个应用的执行过程中使用这类功能部件所占的百分比为fX。那么只有当BWX/fX不小于n时,Superscalar处理器才能够充分的并行,否则该功能部件必将成为瓶颈。因此在微架构的设计中,通常并行设置多个执行较慢的功能部件以提高BWX参数,当然还有一个方法是缩小fX参数。如何缩小fX参数并不是微架构的关注领域,因为在一个给定的应用中,从微架构设计的角度上看,fX参数没有太大的变化空间。

我们可以将BWX/fX公式扩展到存储器读写指令。假设BWS为提供给指令流水线的存储器访问带宽,而fM为存储器读写指令所占的比例,那么只有在BWS/fM不小于n时,存储器访问单元才不会成为指令流水线的瓶颈。

为此我们建立一个基本的存储器访问模型。为简化起见,假设在一个Superscalar处理器中,存储器结构的最顶层为L1 Cache,其中L1 Cache由指令和数据Cache两部分组成,并使用Write Back方式进行回写,L1 Cache通过L1-L2 Bus与L2 Cache连接。L2 Cache与主存储器系统直接连接。处理器在访问存储器时,首先通过L1 Cache之后再经过L2 Cache,最后到达主存储器系统。该模型也可以进一步扩展到L3 Cache和更多的Cache层次,但是为了简化起见,我们仅讨论L1和L2 Cache的情况。在这个前提之下,我们讨论与Cache相关的延时与带宽,重点关注Cache Hierarchy为指令流水提供的有效带宽,即BWS参数。

当微架构进行存储器访问时,将首先访问L1 Cache,此时有Hit与Miss两种情况。如果为Hit,L1 Cache将直接提供数据;如果为Miss,L1 Cache将产生一个Miss请求,并通过L1-L2 Bus从L2 Cache中获得数据。

通常情况下Cache Miss会引发Cache Block的Replacement,如果被替换的Cache Block为Dirty时,还需要向L1-L2 Bus提交Writeback请求,此时L1 Cache Controller将向L2 Cache发送两类数据请求,一个是Cache Miss Request,一个是Write Back Request。为了提高Miss Request的处理效率,在绝大多数微架构中首先向L2 Cache发送Cache Miss Request,之后再发送Write Back Request。

由上文的分析可以发现,BWS参数不能通过简单的计算迅速得出,必须要考虑整个Cache Hierarchy的实现方式。BWS参数与L1-L2 Bus的实现方式,与是否采用了Non-Blocking Cache的设计密切相关。

在现代微架构中很少再继续使用Blocking Cache实现方式,但是我们依然需要分析为什么不能使用这种技术。在使用Blocking Cache的微架构中,存储器访问如果在L1 Cache中Hit时,可以直接获得数据,不会影响指令流水。

但是出现Cache Miss时,由于Cache Blocking的原因,L1-L2 Bus将被封锁,直到从其下的存储器子系统中获得数据。由于采用这种技术L1-L2 Bus一次仅可处理一个L1 Cache Miss,如果出现多个Cache Miss的情况时,这些Miss请求将逐一排队等待上一个Miss获得数据。这种做法严重降低了BWS参数,从而极大影响了Superscalar处理器的并行度。

从以上说明可以发现Blocking Cache的主要问题是处理Cache Miss时,使用了停等模型,L1 Cache最多仅能处理一个Pending的Miss请求。而L1-L2 Bus一次也只能处理一种数据请求,或者是Miss请求,或者是Writeback请求。

在否定Blocking Cache之前,我们需要对BWS参数做进一步的分析。假设在一段程序中一共进行了H+M次存储器请求,其中H是L1 Cache的Hit次数,而M为Miss次数。由于本次模型规定存储器访问不得Bypass L1 Cache,所以在L1 Cache中Hit和Miss次数之和即为存储器访问请求的总和。

在现代处理器中L1 Cache通常设置两个Ports,但是为了简化模型,我们假设在L1 Cache中只含有一个Port。假设L1 Cache在Hit的情况下,一个Cycle即可将数据读出,此时处理H个Hit操作,L1 Cache一共需要使用H个Cycle,如果所有存储器访问都能命中L1 Cache,BWS参数为1。但是L1 Cache不可能永远Hit,因此在计算BWS参数时,必须要考虑M个Cache Miss的情况。

L1 Cache处理一次Miss所需的时间为TM+B,而L1-L2 Bus在处理一次Miss所需的时间为[TM+B(1+d)]。其中TM是指L1 Cache发出Miss请求之后,L2 Cache可以提供有效数据Block的时延;B指L2 Cache通过L1-L2 Bus向L1 Cache提交一个数据Block的时间;d指需要进行Writeback请求的比例。

Blocking Cache中,由于Hit和Miss操作不能流水处理,因此L1 Cache和L1-L2 Bus在处理H+M次存储器请求时,一共需要(H+M[TM+B])和M×[TM+B(1+d)]个Cycle。在这样一个处理器系统中,BWS参数为L1 Cache和L1-L2 Bus所提供带宽的最小值,如公式2‑5所示。

2.6 <wbr>Cache <wbr>Never <wbr>Block

其中m为Miss Ratio,即M/(H+M)。由公式2‑5可以发现,当m增加时BWS参数将等比例降低。考虑m等于0.05,TM等于10,B等于1,d等于0这个理想状态时,BWS参数也仅为0.67。此时如果fM参数为0.4,BWSfM参数的比值为1.67。由此可以得出在这种模型下,一个Superscalar处理器不管内部具有多少可以并行执行的模块,但是一次发射的指令都不能超过两条,否则存储器读写必将成为瓶颈,最终阻塞整个流水线。这一发现使Superscalar处理器因为Cache Blocking的原因几乎无法继续发展。

因此必须有效的提高BWS参数。提高BWS参数有两个有效途径,一个是增加命中L1 Cache后的带宽,也由此带来了多端口Cache的概念。正如上文的讨论结果,Cache上每增加一个端口,需要耗费巨大的开销,而且Cache容量越大,这类开销越大。在现代处理器中,L1 Cache多由两个端口组成,如图2‑9所示。为了进一步提高并行度,Superscalar处理器还可以使用更多的端口,但是这将使用更多的资源。

在对Cost/Performance进行Trade-Off之后,Cache设计引入了Multi-Bank的概念。一些量化分析的结果证明,与单纯的Multi-Port Cache实现相比,Multi-Bank与更少的Port组合在经过一些简单的消除Bank Conflict的优化之后,可以获得更高的Cost/Performance比[50]。这使得MBMP(Multi-Banks and Multi-Ports)结构在现代微架构的Cache设计中得到了广泛应用。Opteron的L1 Data Cache使用了8-Banks 2-Ports的组成结构[6]。

这些优化依然只是提高了Cache Hit时的Cache带宽。如公式2‑5所示,计算Cache的总带宽需要考虑Cache Miss时的情况。因此即便我们将Hit时的Cache带宽提高到Infinite,依然不能解决全部问题,必须要考虑1/(m×[TM+(1+d)×B])这部分带宽。

m和TM参数都非常小,而且B等于1时,这部分带宽并不值得仔细计算。但是事实并非如此,随着计算机主频的不断攀升,TM参数的相对值一直在不断提高;而主存储器的快速增加,L1 Cache相对变得更小,使得m参数进一步提高。这使得Miss时使用的带宽备受关注。最自然的想法是使用流水方式将多个Outstanding Cache Miss并行处理,使得多个Miss可以Amortize TM参数,从而最终有效提高BWS参数。

这一构想使David Kroft在1981年正式提出了Lockup-Free Cache(Non-Blocking Cache)[51]这个重要的概念。Kroft建议设置一组MSHR(Miss Information/Status Holding Registers)暂存Miss请求。其中每一个MSHR与一个Outstanding Miss相对应。当MSHR的个数为N时,该Cache结构即为Non-Blocking(N) Cache。Non-Blocking(N) Cache可以并行处理N个Outstanding Cache Miss,而且在处理Cache Miss的同时可以并行处理Cache Hit,修正了Blocking Cache存在的诸多问题,使得Superscalar处理器得以继续。

David Kroft,这个毕生都没有发表过几篇文章的,也许是普通的再也不能普通的工程师学者,让众多拥有几十篇,甚至几百篇的教授和Fellow汗颜。他在Retrospective中这样评价着这个算法。

How does one begin to describe the dreams, thoughts and fears that surround a discovery of a different view of some old concepts or the employment of old accepted methodology to new avenues? It is probably best to start the account by describing the field of Computer Architecture, in particular, the area of hierarchical memory design, that was prevalent around and before the time the ideas came to light.

...

As indicated at the beginning, a discovery is just a different look at some old concepts or the use of some old concepts for new mechanisms. Mine was the latter [52].

从今天对Cache的认知上看,David Kroft的建议顺理成章,几乎每一个人都可以想到。而在David Kroft提出这个设想的那个年代,Miss Blocking几乎是理所应当的,可以极大简化Cache的设计。David Kroft所以能够提出这个建议更深层次的原因,是他具有向已知的结论挑战的勇气。

我目睹过一些填鸭式的魔鬼训练营。那里逼迫着诸多才智之士强记着几乎不会出错的结论,强化着比拼编码速度的各类实现。直接使用结论比从学习基础的原理并推导出结论快得多。按部就班是过于漫长的,是无法速成的。这样做似乎是一条捷径。只是伏久者,飞必高;开先者,谢独早。世上没有捷径,起初的捷径必为将来付出惨痛的代价。

依靠速成得来的这些结论和已知实现虽然可以被信手拈来,用于构建各类设计,却更加容易使人忽略一些更加基础的知识。最重要的是这些结论会很容易在心中生根发芽。在经过多次反复后,这些才智之士不会没有挑战结论的勇气,只是失去了挑战结论的想法。诸恶之恶,莫过于此。以中国的人口基数,却远没有获得与此对应的成就,在Cache Memory,处理器,计算机科学,在更多的领域。生于斯,长于斯,愧于斯。

David Kroft提出了Non-Blocking Cache的同时,打开了潘多拉魔盒,这个领域奋战着的Elite万劫不复。在Lockup-Free Cache出现后的不长时间内,经历了许多修改,使得CPU Core-L1 Bus,L1-L2 Bus,其后的Bus设计进一步复杂化。

David Kroft提出的算法只有4页纸的描述,更多的是春秋笔法,以至于很难根据这个论文,实现出可用的Non-Blocking Cache结构。这些已不再重要。David Kroft当时的想法基于当时的认知,很难突破当时的认知,绝非完美,可能只适用于Statically-Scheduled架构,依然指明了前进之路。其后Dynamically-Scheduled架构进一步改进了Lockup-Free Cache结构。这也是现代微架构在进行Cache设计时常用的方法。

对于Dynamically-Scheduled的处理器,Non-Blocking Cache有多种实现方法,Address Stack,Load Queue,Address-Reorder Buffer或者其他方法。本篇仅介绍Address Stack实现机制,该机制的实现较为直观,其基本组成结构如图2‑21所示。

2.6 <wbr>Cache <wbr>Never <wbr>Block

以上Non-Blocking Cache的实现中,包含5个MSHRs,Address Stack的Entries数目为16。在这种实现中,如果微架构访问存储器出现Cache Miss时,需要首先判断本次访问所需要的数据是否正在被预取到Cache Block中,该功能由MSHRs寄存器组及相应控制逻辑实现;当数据从存储器子系统中返回时,需要解决所有依赖这个数据的Pending Cache Miss,该功能由Address Stack及相应的控制逻辑实现。

每个MSHR至少包含3个数据单元,Block Valid Bit,Block Address和Destination Bits。其中Block Valid Bit有效表示地址为Block Address的Cache Block发生过Fetch,从IC Design的角度上看,Cache Block是有地址的,上文已经对此进行过描述。

Cache Miss时,Cache流水线将首先并行查找MSHRs寄存器组的Block Address,判断即将访问的Cache Block是否正在被Fetch。如果在MSHRs寄存器组中命中,本次Cache Miss即为Secondary Cache Miss,否则为Primary Cache Miss。

对于Primary Cache Miss,需要在MSHRs寄存器组中获得一个空闲Entry,并填写相应的Block Address,设置Block Valid Bit,同时向其下的存储器系统发送Fetch请求;如果当前MSHRs寄存器组中没有空闲Entry,之后微架构将不能继续issue存储器请求,Cache流水线将被Stall,直到之前发出的Fetch请求返回,释放MSHRs寄存器组中的对应Entry。

Secondary Miss的处理相对较为复杂。此时需要视不同情况讨论。不同的微架构采用了不同的策略处理Secondary Miss。在Kroft的建议中,Cache Block的管理按照Word进行分组。因此只有Secondary Miss访问的Word并与Pending Miss请求完全一致时,Cache流水线才会Stall,这种MSHRs也被称为Implicitly Addressed MSHRs[54]。与此对应的概念还有Explicitly Addressed MSHRs,采用这种组成方式,可以解决对同一地址进行访问时,不会Stall Cache流水线,其详细实现见[54]。

一个很容易想到的,更加高效的方法是将Primary Miss和其后若干个Secondary Miss合并为一个Fetch请求。但是这种Merge是有条件的,需要更多的Buffer实现。这种方式不仅带来延时,更为Memory Consistency制造了不小的麻烦。

Cache Miss引发的Fetch请求最终返回,并从其下的存储器系统中获得Cache Block之后。Cache流水线将并行检查MSHRs寄存器组,并获得对应的Destination Bits,并依此判断,所获得的数据是发向指令Cache,数据Cache,还是某个定点或者浮点寄存器。然而在Dynamically-Scheduled架构中,为了维护Cache Consistency,问题更加复杂。

Dynamically-Scheduled架构中,由于Load Speculation的存在,存储器读操作在微架构出现Mispredicted的转移指令,或者Exception时,需要抛弃之前的Speculative Load请求,此时可以采用Address Stack处理这种情况。

Address Stack为Fully-Associative Buffer,由Address Valid Bit, Physical Address, Instruction ID三个字段组成。当微架构发射存储器读写指令时,将首先计算Physical Address,之后并行查找Address Stack,获得空余的Entry。如果Address Stack中没有空余的Entry,将产生Structural Hazard,已发射的存储器指令不断重试,直到获得空余的Entry;如果有空余,则设置该Entry的Address Valid Bit,填写Physical Address,并向其下存储器系统发送Fetch请求。

Fetch的Cache Block返回时,会并行查找Address Stack,如果Match,返回的数据将进入相应的Cache,可能还会传递给某个寄存器。指令流水线的设计者可以决定是在存储器读写指令在Complete还是在最后的Commit阶段时,才真正释放Address Stack的Entry。前者实现较为复杂,后者较为简单。

在一切都是“正常”的情况下,返回的Cache Block并行查找Address Stack似乎是冗余的。但是考虑Misprediction转移和Exception处理之后,我们不能得出这样的结论。当发生这两种情况时,在刷新微架构状态时,所有Speculative Store/Load指令也需要被刷新。

使用Address Stack方法设计Non-Blocking Cache时,处理这些异常需要清除相应Entry的Address Valid Bit。当数据返回时,将无法在Address Stack中找到合适的Entry。对于存储器读请求,数据将不会写入Physical Register,对于存储器写请求,数据不会真正写入,以避免各类潜在的错误。

由以上描述,可以发现这种Non-Blocking设计由两部分组成,一个是Cache Miss时使用的Address Queue,和处理数据返回使用的Address Stack。不同的微架构使用的Non-Blocking Cache设计方法大同小异,要点是设置专门的Buffer处理多个Cache Miss,采用流水方式,使多个Cache Miss请求最终可以Amortize TM参数,最终有效提高BWS参数。

Non-Blocking Cache的概念不难理解,重要的是实现。总线技术的不断发展,使得这些实现愈发困难。在现代微架构中,用于Cache间互联的总线,大多被分解为若干个子总线,由Data,Request,Ack和Snoop四条子总线组成,而在这四条子总线中还会继续分级,以进一步扩展带宽。这些变化使得Non-Blocking Cache的话题离学术界渐行渐远。

在系统总线设计中出现的这些变化极大增加了Cache流水线的设计难度。我们进一步考虑Cache的层次结构从L1,L3到EDRAM,进一步考虑处理器系统从CMP,SMP到ccNUMA,进一步考虑Memory Consistency从Strict consistency,Sequential Consistency到Weak Consistency,进一步考虑各类细节和不断攀升的Cache运行频率,会使最具睿智的一群人为之魂系梦牵。他们明白世上再无任何数字逻辑能够如此忘情。

Cache流水线与协议的复杂,触发了如何解决State-Explosion的问题,这使得Cache 的Verification也成为了一个专门的学问,引发了无尽的讨论。每在Cache流水线面前,心中想的总是成千上万的服务器日夜不息的忙碌,持续追求完美的设计在最高的工艺上不断进行的深度定制。

面对着这一切,任凭谁都显得那样的微不足道。

posted @ 2014-07-29 18:41  Gui Kai  阅读(580)  评论(0编辑  收藏  举报