《浅谈Cache Memory》 学习-第四章
Cache的层次结构
我第一次接触存储器瓶颈这个话题是在上世纪九十年代,距今已接近二十年。至今这个问题非但没有缓和的趋势,却愈演愈烈,进一步发展为Memory Wall。在这些问题没有得到解决之前,片面的发展多核,尤其是片面提高在一个CMP中的CPU Core数目几乎没有太大意义,除非你所针对的应用是风花雪月的科学计算。在越来越多的应用领域中,在一个CMP中提供的多个处理器内核很难全部发挥作用,造成了不容忽视的资源浪费。这与很多因素相关,我们不要贸然尝试如何去解决这些问题,需要了解这些问题因何而来。
4.1 Cache 层次结构的引入
我经常尝试一些方法,试图去解决在存储器子系统存在的瓶颈问题,总会陷入长考。在我们所处的这个领域,这个时代,在不断涌现出一些新的变化。这些变化会我们之前历千辛万苦学得一些知识和理念荡然无存。这些变化不会依赖你的喜好而改变。
1975年,Moor将其定律修正为“晶体管的数目每18月增加一倍,性能也将提高一倍”。这个定律在维持了相当长一段时间的正确性之后逐步失效。首先体现在性能提高一倍上,使用过多核编程的人不敢轻易地说用3个处理器一定能到达单处理器乘2的效果,不用说2个核。更多的领域和应用证明并正在不断证明“18个月性能提高一倍”不断着发生错误。
晶体管的数目每18个月增加1倍也遭到了前所未有的挑战。Intel的Tick-Tick计划努力维护着摩尔定律最后的领地,也无法改变最后结果。从已知的几乎全部领域看,技术的发展是初期爆发而后逐步平缓,从更长的时间段上分析将呈对数增长。摩尔定律不会例外只会在一段时期适用。这段时光将是属于Intel和半导体界最美好的岁月。
在不远的将来,相同的Die所容纳的晶体管数目将是有限的。在时光飞舞中,这个将来距离我们这个年代很近,因为永远尚不足够遥远。摩尔定律的失效近在咫尺,这个失效率先对目前云集在处理器领域的人群产生影响。
传统意义的多核时代已经逐步结束。在半导体技术所依赖的基础领域发生重大的更新与变革之前,使用更多的晶体管资源扩大处理器数量并不可取。在设计多核处理器公司中工作的一些朋友经常和我提起,今年或者明年他们就可以集成更多的处理器内核向Intel发起挑战。我想和他们说以相同的Die,Intel可以集成更多那样的处理器内核,集成256,512或者更多的处理器内核对Intel都不是太大的难题。只是不知这样做的目的何在,很多应用甚至无法发挥4个或者8个内核的性能。
即便不谈这些性能问题,我们讨论功耗问题。有很多许多工程师在这个领域中孜孜以求,默默耕耘着,针对功耗进行各类优化。很多工程师在做这件事情时,忽略了一些在其他领域中的基本常识,甚至是能量守恒定律。
忽略这些常识的结果是所做努力最后灰飞烟灭。在处理器领域,完成一件事情可能由多个任务组成,各类降低功耗手段的出发点是在完成这件事情时,统筹考虑多个任务的执行过程,避免重复性的工作。
从这个角度上看,微架构在Misprediction时,产生的重复执行操作是最大的功耗来源,还有在多个任务执行过程中,与当前任务执行无关的功能部件的开启等,这些都是具体的优化细节。围绕着处理器功耗优化,有许多需要作出努力的工作细节,需要更多的人来参与。只是再多的人也注定不够。截止到今天,处理器设计的出发点还是为了能够解决更多的应用,希望拥有更多Feature来怀抱天下。
这种设计思路从处理器诞生的第一天开始,那个年代距今已经较为遥远。处理器及其相关应用领域的发展与革新是二战之后人类文明进步重要的源推动力。其他领域在可望不可及过程中产生的无限向往,使得几乎剩余的所有应用迅速向这一领域靠拢。也因此产生了通用处理器,使处理器更加聪明,更加通用。
处理器系统正是这样逐步通用化。Intel是产生这种通用处理器的最典型和最庞大的厂商。而目前发生和正在发生的诸多事实表明这种通用化已经举步维艰,诸如Memory Wall,功耗居高不下这样的问题已经触及通用处理器是否能够继续发展的底线。
越来越多的领域提出了应用为王这样的口号,本质上这是一个使处理器不再继续通用的口号。更多的领域需要属于自己的定制,不再完全继续依托通用处理器。在这些领域,通用处理器仅作为一个基本组成模块,更多的是适合这个领域的定制逻辑。
处理器领域在经历了爆发式增长,快速增长后回归自然,使得处理器不再神秘,使得一些电子类产品的使用习惯正在进一步回归。苹果近期辉煌源自于此。Jobs再次来到苹果后只做了一件事情,使MP3更像MP3,电话更像电话,平板更像平板。苹果的成功在于Jobs使一个没有受到这些来自处理器领域的专业或者半专业人的影响之下,正常的使用这些设备。这不是创新,是回归自然。任何人,任何群体,任何公司都无法阻挡这股天然的力量。
在此之前,任何一款电子类产品都自觉或者不自觉的受到了来自处理器领域的影响。许多设备,甚至是许多嵌入式设备亦无法幸免。原本就应该属于应用领域的设计并没有按照自身领域的特点设计,被来自通用处理器领域的想法左右。在过去的岁月中,那片领域曾经多次证明引入通用处理器观点后获得了巨大的成功。这种成功在某种程度上桎梏了观念,使更多的人容易忘记在应用领域中所真正应该的关注。
这些固有的观念无法阻挡专用化时代的脚步。专用化和定制化已经出现在通用处理器领域中,比如在加密算法实现领域使用的专用引擎,用于图像处理的GPU等等。近期会出现更多的加速引擎,会进一步弱化通用处理器。可以说这种专用化和定制化时代已经来临,通用处理器所覆盖的范围会越来越小。
从这个角度上说,Intel之忧不在功耗性能比没有超过ARM的Cortex系列处理器,而是离通用处理器日益远去的各类应用,继续坚持使用更加通用的处理器,以包含更多的应用会遭遇比Memory Wall更加牢不可破的Wall,迎接几乎必败的格局。如果ARM架构不是进一步为应用预留空间,去采用通用方法去左右这些应用,通用处理器所面临的困境将如期而至。
在通用处理器领域中,ARM架构的成功是一个莫大的讽刺。在其成功背后,最主要的原因是在使用ARM的许多应用中,ARM微架构不是如想象中那样重要。我们可以简单列举出在使用ARM构架中最伟大的几类电子产品,其中哪怕有一个是因为单纯使用ARM架构而伟大。ARM公司的高明之处是轻架构而重应用,由环绕其总线组成的Ecosystem势不可挡,率先开启了专用化时代,ARM微架构不过是一个载体。
进一步定制化不意味着通用处理器将很快退出历史舞台。对于某类应用,定制辅以通用处理器的组合将长期存在,这种场景之下,定制的与应用紧密结合的部分成为主角。较为理想的情况是95%的定制与5%的通用处理器。这有助于解决存储器Wall和功耗问题。在这种情况下,处理器将不会频繁访问主存储器,即使访问也是在5%的基础上,也不用担心功耗问题,因为只有5%的权重。
这样的结果是不是将全部负担转移到了定制化部分。不用为此做太多担心,我反对使用通用处理器所做的某些应用,是基于处理器过于通用而产生了过多的不需要的存储器访问,引入了并不需要的功耗。贴近应用的定制无法做到不进行数据传递,也无法避免不消耗能量,但是能够将这种影响限制到最小的范围之内。附着在处理器系统上的操作系统,也会因为这些变化而改变。从通用性的角度上看,x86处理器和Windows操作属于一类产品,只是一个在处理器领域,一个在操作系统领域。这两个产品处于同一个困境中。
在不久的将来也许会出现适用于通用部分的操作系统,这个操作系统与传统操作系统,如Windows和Linux,这些操作系统所做的工作较为类似,却在不断弱化,Linux进入到2.6内核之后,再无实质变化,这是通用操作系统所面临的同一个问题。另一个部分操作系统将与定制相关,也被称为应用类操作系统,这些操作系统将有很多种类。
有人会问,既然你认为定制化的时代已经开启,为什么花如此气力在通用处理器的Cache中。因为他山之石可以攻玉。通用处理器的发展借鉴了很多领域的精华,在定制即将兴起的时代,不能忽略通用处理器的核心。定制化领域依然会从狭义广义Cache的设计思想中受益。Cache的核心是数据缓冲组织结构,通路连接方法,管理策略和各类算法,这些内容较为基础。世间万物,千变万变,基础内容的变化较为缓慢。
使用定制会缓解通用处理器做遇到的各类瓶颈,而不是消除。主存储器的容量和延时的不断增加依然是客观存在的事实。通用处理器领域的解决方法是引入更多级别的Cache。多核处理器的进一步发展使得多级Cache间的组成结构异常复杂。
在单处理器系统中,Cache Memory由多个层次组成。这些层次相互关联,相互依托,而为参天大树。底部由主存储器与外部存储器组成庞大的根系,其上由各类Cache和各类用于连接的Buffer,形成茂密的枝干。这些参天大树更通过各种类型的网络,或松或紧连接在一起,若小为林,夫广为森,煌煌立于天地,善能用之,攻坚强者莫之能胜。在一个ccNUMA处理器系统中,典型的Cache Memory层次结构如图4‑1所示。
不断增加的主存储器容量需要更大的Cache减少Miss Rate,使用一个很大的L1 Cache无法胜任这个工作,主存储器不仅在变大,访问延时也在逐步提高,进一步加大了指令流水线间的差距。单独一级Cache无法掩盖这个差距。提高Cache的访问延时与进一步扩大容量是一个Trade-Off。而且Cache容量越大,其访问延时也越大。
这些现状使得在现代处理器系统中引入多级Cache成为必然。在没有从根本上解决存储器子系统存在的这些问题时,Cache级数会逐步提高,目前大多数中高端处理器都已经包含了L3 cache,大规模地使用L4 Cache并非遥不可及。
在Cache Memory层次结构的最顶端为微架构中包含的系统寄存器和各类Buffer,如与Data Cache连接的LSQ和与Instruction Cache连接的I-Cache Line Filling部件中的Buffer。FLC(First-Level Cache)指与微架构直接进行连接的Cache。在许多微架构中,FLC指L1 Cache,而在有些微架构中,在L1 Cache之上,存在一级L0 Cache,此时这个L0 Cache即为FLC。
LLC指在处理器系统中与主存储器直接相连的Cache。在不同的处理器系统中,LLC所指对象迥异,有的处理器使用L2 Cache连接主存储器,此时L2 Cache为LLC;有的使用L3 Cache,此时L3 Cache为LLC。有些处理器系统中,使用L1 Cache连接其上的微架构,同时连接其下的主存储器,此时L1 Cache为FLC的同时也为LLC。
MLC其上与FLC相连,其下与LLC相连。在一个复杂的处理器系统中,Cache Memory层次结构由L0~L3 Cache和EDRAM组成,其中L0 Cache与微架构直接相连,而EDRAM与主存储器相连。此时L0 Cache为FLC,EDRAM为LLC,而L1~L3 Cache为MLCs。
在处理器系统中,不同级别的Cache所使用的设计原则并不类同。每一级Cache都有各自的主要任务,并不是简单的容量与延时的匹配关系。假设一个处理器系统中含有L1和L2两级Cache。多数程序具有的Temporal和Spatial Locality使得L1 Cache的Hit Rate非常高,即便L1 Cache只有4KB或者32KB。这使得在L2 Cache层面并不会看到过多的Miss[70],这使得L2 Cache层面的优化重点并不是匹配容量与延时,而且L1 Cache的Miss Rate越低,L2 Cache的延时越无关紧要[70]。
这使得在绝大多数微架构中,对于L1 Cache的优化专注于提高Hit Time,在现代处理器中其范围在3~5个Cycle之间,为了避免在这个Critical Path上虚实地址转换的开销,很多微架构,如Cortex A8/9和Opteron,直接使用了Virtual Cache。在L2 Cache层面,其Cycle与容量比大于L1 Cache的对应比值。这些设计方法使得Cache Hierarchy的设计在与直接使用一级Cache的整体Miss Ratio等效时,提高了Hit Time这个重要的参数。
在不同的微架构中,L1与L2 Cache的关系可以是Inclusive也可以是Exclusive或者其他类型,如果是Exclusive关系,计算L2 Cache的有效容量需要累加L1 Cache的容量。在L2 Cache层面中,具有更多的时间,可以实现更多的Way Associatively进一步降低Miss Ratio。
在现代处理器中,这个L1和L2 Cache通常是私有的。LLC的实现需要考虑多核共享的问题,除了继续关注Hit Time,Miss Ratio和Miss Penalty这些基本参数之外,需要重点考虑的是Scalability,由多核处理器引发的Bus Traffic等一系列问题。
在一个大型处理器系统中,不同种类的Cache需要通过Cache Coherent Networking进行连接,组成一个复杂的ccNUMA处理器系统。这个Cache Coherent Networking可以采用多种拓扑结构,可以使用MESH,n-Cube,也可以是简单的树状结构,环形结构。在整个ccNUMA处理器系统中,这个Cache Coherent Networking的搭建异常复杂。
比这个Networking更难实现的是Cache间的一致性协议,这个一致性策略可以使用纯硬件,也可以使用软件Message方式实现。Cache每多引入一级,这个一致性协议愈难实现愈难验证。其间的耦合关系设计复杂度已经超出许多人的想象。
Cache Hierarchy还与主存储器间有强的耦合关系,延时与带宽是两者间永恒的话题。在这个话题之后的功耗亦值得密切关注。在主存储器之下是外部存储器系统,包括SSD(Solid State Disk),磁盘阵列和其他低速存储器介质。外部存储器系统的复杂程度不但没有因为相对较低的速度而减弱,而且这种低速使得设计者有着更大的回旋余地,可以设计出远超过Cache层次结构使用的的复杂算法。如上文提及的LIRS和Clock-Pro页面替换算法。在这个领域没有什么算法是万能的,所有算法都有自身的适用范围。
在讲述这些复杂的设计之前,我们需要从更加基本的内容开始,需要回顾存储器读写指令的执行过程,在微架构中使用的LSU部件是Cache Hierarchy设计的最顶端。在多数微架构中,LSU,AGU和ALU协调工作实现存储器读写指令的发射与执行。
4.2 存储器读写指令的发射与执行1
在CPU Core中,一条存储器指令首先经过取值,译码,Dispatch等一系列操作后,率先到达LSU(Load/Store Unit)部件。LSU部件可以理解为存储器子系统的最高层,在该部件中包含Load Queue与Store Queue。其中Load Queue与Store Queue之间有着强烈的耦合关系,因此许多处理器系统将其合称为LSQ。在多数处理器的存储器子系统中,LSU是最顶层,也是指令流水线与Cache流水线的联系点。
LSU部件是指令流水线的一个执行部件,其上接收来自CPU的存储器指令,其下连接着存储器子系统。其主要功能是将来自CPU的存储器请求发送到存储器子系统,并处理其下存储器子系统的应答数据和消息。在许多微架构中,引入了AGU计算有效地址以加速存储器指令的执行,使用ALU的部分流水处理LSU中的数据。而从逻辑功能上看,AGU和ALU所做的工作依然属于LSU。
在一个现代处理器系统中,LSU部件实现的功能较为类同。LSU部件首先要根据处理器系统要求的Memory Consistency模型确定Ordering,如果前一条尚未执行完毕存储器指令与当前指令间存在Ordering的依赖关系,当前指令不得被Schedule,此时将Stall指令流水线,从来带来较为严重的系统惩罚;LSU需要处理存储器指令间的依赖关系,如对同一个物理地址的多次读写访问,并针对这种Race Condition进行特殊优化;LSU需要准备Cache Hierarchy使用的地址,包括有效地址的计算和虚实地址转换,最终将地址按照L1 Cache的要求将其分别送入到Tag和状态阵列。
L1 Cache层面需要区分是存储器读和存储器写指令。无论是存储器读还是写指令穿越LSU部件的过程均较为复杂。为缩短篇幅,下文重点关注存储器读指令的执行。存储器读操作获得物理地址后将进行Cache Block的状态检查,是Miss还是Hit,如果Cache Hit,则进行数据访问,在获得所需数据后更新在LSU中的状态信息。
如果在L1 Cache中Miss,情况略微复杂一些。在现代处理器系统中,Non-Blocking Cache基本上是一个常识般的需求,为此首先需要在MSHR中分配空余Entry,之后通过L1 Cache Controller向其下Memory Hierarchy发送Probe请求。
在L1 Cache Controller中,大多使用Split Transaction发送这个Probe请求,之后经过一系列复杂的操作,这个操作涉及多核之间的Cache一致性,不同的一致性协议对此的处理不尽相同。在获取最终数据之后,回送Reply消息。LSU在收到这个Reply消息后将数据传递给指令流水线,并释放MSHR中的对应Entry。
以上这些操作可以并行执行,如使用Virtual Cache方式可以直接使用虚拟地址,而无需进行虚实地址转换,可以将数据访问与Tag译码部件重叠,更有一系列预测机制进一步缩短数据Cache访问的延时。
存储器写的过程较存储器读复杂一些,在L1 Cache Hit时,会因为状态位的迁移而带来一系列的Bus Traffic;如果在L1 Cache Miss,要首先取得对访问数据的Ownership或者Exclusive。获得Ownership的步骤与存储器读发生Miss时类似,但是只有在存储器写Commitment时,才能将数据真正写入到Cache中。
在微架构的设计中,缩短Cache的访问延时与提高带宽是Cache流水设计的一个永恒话题。这些话题可以空对空的讨论,搭建各类模型得出最后的量化分析报告。但是这些报告面对着ccNUMA处理器多级Cache设计的复杂度时,显得如此苍白无力。
下文以AMD的Opteron微架构为主,进一步说明存储器读写的执行过程。我们从Cache Hit时的Load-Use Latency这个重要参数的分析开始,进一步介绍Cache Hierarchy性能的评价标准,最后详细说明Opteron微架构的LSU部件的工作原理。
在多数微架构的数据手册中,Load-Use Latency的计算是从指令进入LSU之后开始计算,以指令从Cache Hierarchy中获得最终数据作为结束。在不同的场景,如其下的Cache Hit或者Miss时,所得到的Load-Use Latency参数并不相同。
在Cache Hit时,AMD Opteron微架构凭借着Virtual Cache,使得其L1 Cache的Load-Use Latency仅为3个Cycle[6],这是一个非常快的实现。在Intel近期发布的Sandy Bridge中,L1 Cache的Load-Use Latency也仅为4个Cycle[69]。这个参数不能完全反映在一个处理器系统中Cache Hierarchy的整体Hit Time参数和存储器平均访问时间,远不能仅凭这一个参数得出Opteron的Cache Hierarchy结构优于Sandy Bridge微架构的结论。
从许多Benchmark的指标上看,Sandy Bridge微架构在Cache Hierarchy中的表现远胜于Opteron,Virginia STREAM的测试结果表明Opteron在Copy,Scale,And和Traid测试指标上优于Pentium4,却落后于使用Nehalem Xeon的Apple Mac Pro 2009,如表4-1所示。
表4-1 STREAM "standard" results[71]
Data |
Machine ID |
ncpus |
COPY |
SCALE |
ADD |
TRAID |
2000.12.23 |
Generic_Pentium4-1400 |
1 |
1437.2 |
1431.6 |
1587.7 |
1575.4 |
2005.02.04 |
AMD_Opteron_848 |
1 |
4456.0 |
4503.6 |
4326.3 |
4401.4 |
2009.10.23 |
Apple_Mac_Pro_2009 |
1 |
8427.6 |
8054.4 |
8817.4 |
8953.4 |
Copy,Scale,And和Traid是Virginia STREAM定义的4个操作。其中Copy操作与a(i) = b(i)等效;Scale与a(i) = q×b(i)等效;Add与a(i) = b(i) + c(i)等效;而Traid与a(i) = b(i) + q×c(i)等效。由以上操作可以发现,STREAM的测试结果不仅和Cache层次结构相关,而且与主存储器带宽和浮点运算能力相关。这一测试指标并不能作为Opteron和Nehalem微架构Cache层次结构孰优孰劣的依据。与存储器测试相关的另一个工具是lmbench,不再细述。
这些Benchmark程序在相当程度上反映了Cache和存储器子系统的性能。但是在一个实际项目的考核中依然只能作为参考。对于这个设计,最好的指标一定从自身的应用程序中得出的。很多人会对这个说法质疑,因为许多应用程序的书写异常糟糕,没有用到很多的优化手段。但是面对着由数千万行组成,积重难返的应用程序,更多的人剩下的只有无奈。
在Intel的Nehalem和Sandy Bridge微架构中,FLC访问延时均为4个Cycle[12][69],一个可能的原因是,Intel x86处理器在Pentium IV后并没有采用VIPT(Virtually Indexed and Physically Tagged)方式[12][68]组织L1 Cache,而采用了PIPT(Physically Indexed and Physically Tagged)方式[1]。Opteron微架构使用VIPT方式,访问L1 Cache时可以直接使用Virtual Address,从而可以节省一个时钟周期。但是Opteron的总线位数只有为64位。AMD最新的Bulldozer微架构,将总线位数提高为128b后,Load-Use Latency提高为4。
确定存储器平均访问时间除了需要考虑Hit Time参数之外,还需要考虑Miss Penalty参数,Virtual Cache并不能解决所有问题。当L1 Cache Miss时,Opteron微架构访问PIPT结构的L2 Cache时,依然需要使用TLB,TLB译码过程最终不会省略。
采用Virtual Cache还会带来Cache Synonym/Alias问题,需要设置RTB(Reverse Translation Buffer)解决这一问题。使用这种方法除了要使用额外的硬件资源,而且在多核系统中还引入了TLB Consistency的问题。在一个微架构的设计中,Virtual Cache和Physical Cache间的选择依然是一个Trade-Off。
在抛开Virtual Cache的讨论后,我们专注讨论LSU部件。在现代处理器系统中,为了提高存储器读写指令的执行效率,一个微架构通常设置多个LSU部件,这些LSU不是简单的对等关系,而是有相互的依赖关系。
简单对等相当于在一个微架构中设置了多个功能一致,异步执行的LSU部件。为了保证处理器系统Memory Consistency Model的正常运行,这些异步操作需要在运行的某个阶段进行同步处理,从而带来了较大的系统复杂度。这不意味着不采用这种对等设计可以避免这些同步问题,只是相对容易处理。
在多数微架构中,多个LSU部件间通常是各司其职,并非完全对等。Opteron微架构的LSU部件也是采用了非对等的两个LSU部件。在介绍这些内容之前,我们需要了解Opteron微架构的基本组成结构和流水线示意,如图4‑2所示。
在Opteron微架构中,存储器指令通过Fetch,decode,Dispatch阶段,并在所有Operands准备就绪后,经由Scheduler launch到对应的执行部件。上图中的Res部件全称为Reservation Station,这是Scheduler的一个重要组成部件。其中存储器读写指令需要使用Integer Scheduler,在这个Scheduler中包含一个AGU和一个ALU。
Opteron微架构与存储器指令执行相关的部件由Integer Scheduler,Load/Store Queue,L1 Cache和其下的存储器子系统组成。Load/Store Queue即为上文多次提及的LSQ,是Opteron LSU部件的重要组成部分。在LSU部件中除了具有LSQ之外,还有许多异常复杂的控制逻辑和与指令流水线进行数据交换的数据通路,是一个微架构的存储器子系统设计的起始点,是存储器指令的苦难发源地。
[1] 尚无可靠详细资料说明Sandy Bridge的L1 Cache是VIPT还是PIPT。
4.3 存储器读写指令的发射与执行2
在x86处理器系统中,存储器指令大体可以分为F(reg, reg)[1],F(reg, mem)和F(mem, reg) 三大类。在这些指令中,第1个Operand为目标操作数也可以为源操作数,第2个Operand只能为源操作数。与LSU部件直接相关的指令为F(reg, mem)和F(mem, reg)。后一类指令的处理相对较为复杂,该类指令需要首先进行存储器读,进行某种运算后,再次进行存储器写,即Read-Modify-Write μop。在Opteron微架构中,浮点和SSE指令的存储器读写依然需要通过Integer指令流水,为简化起见,下文不再讨论浮点和SSE指令的存储器读写指令。
一条存储器指令的执行需要使用AGU,ALU和LSU三大部件。AGU,ALU将和LSU部件协调流水作业,最终完成一次存储器读写操作。L1 Cache的Load-Use Latency参数的计算是从存储器指令进入LSQ开始计算。为了实现指令流水的并发高速运转,在AGU和ALU中也具备多级流水结构。存储器指令在通过ALU,AGU与LSU部件的过程中存在相互依赖关系,如图4‑3所示。
我们分别讨论F(reg, mem)指令和F(mem, reg)指令的执行过程。在RS(Reservation Station)等待的F(reg, mem)指令,当所需的Operands Available后首先Launch到AGU部件,并在地址计算完毕后,将结果发送到LSU部件。但是存储器访问μop是同时进入到AGU和LSU部分中的,只有LSU需要等待AGU的计算结果。
在LSU部件从存储器子系统中获得最终数据之后,将其送给ALU,并最终完成指令的执行。F(mem, reg)指令的执行过程与此相近,只是需要在LSQ中经历两次等待过程,第1次是等待ALU计算的结果,第2次等待Store μop的commit将结果最终写入Cache。F(mem, reg)进行存储器读时的操作与F(reg, mem)一致。
AGU和ALU Pipeline的执行过程较易理解,本篇对此不做进一步的说明。Opteron LSU的设计使用Tag和Data总线分离的方式。Scheduler在监听Result Bus时,发现对应的Tag有效时即可启动LSU和ALU的μop,不必等待Data总线,通常情况下Tag bus上的数据先于Result Bus一个cycle,以Overlap不同的流水阶段。在Opteron微架构中,1个Cycle,可以Launch一个ALU和一个AGU微操作。如图4‑3所示,这两个操作显然没有对应关系。
LSU部件负责与其下的存储器子系统进行数据交互,也是图4‑3所示三大部件中最为复杂的一个部件。在Opteron微架构中,LSU由两个部件组成,分别是LS1和LS2。其中LS1中含有12个Entry,LS2中含有32个Entry,共44个Entry[6]。
LS1和LS2也被分别称为Pre-Cache和Post-Cache Load/Store Unit,绝大多人会认为LS1用于访问L1 Cache,而LS2用于访问L2 Cache。这一说法源自AMD的软件优化手册[73],这似乎是一个权威说法,也在某种程度上善意地误导着读者。这一说法非但并不准确,在某种程度上甚至是错误的。Opteron LSU的结构示意如图4‑4所示。
在Opteron微架构中,一条存储器读写指令在被Dispatch到Integer Scheduler的同时,也会被Dispatch到LS1的相应Entry中等待。LS1通过监听AGU的Tag总线获得必要的信息后,开始真正意义上的执行。这些必要的信息包括,LS1所需要的地址是否能够在下一拍有效,将使用哪一个AGU产生的地址。LS1使用Tag总线的信息虽然不能用于 L1 Cache的Probe操作,却可以实现AGU和LSU的流水执行。
Opteron微架构分离Tag和Data总线是利用流水进行加速的技巧,将AGU的执行分解为两个步骤,以最大化AGU与LSU流水部件间的Overlap。除了AGU部件分离的Tag和Data总线,LSU和ALU也进行了这样的总线分离。
在LS1中的指令在下一拍从Data总线中获得地址后执行,由上文所述,Opteron微架构的L1 Cache具有两个端口,当所访问的数据间不发生冲突的前提下,LS1率先处理最先进入队列的两条存储器读写指令,并将其转交给L1 Cache Controller做进一步处理。
无论是存储器读还是存储器写操作,L1 Cache Controller都需要首先进行Probe操作。读操作发起的Cache Read Probe操作将带回数据。写操作发起的Cache Probe操作,也被称之为Probe-before-Write。进行这个Probe操作之前,需要准备好待写的Cache Block,Probe操作返回时将会带回数据,而当且仅当存储器写指令获得最终数据而且进行Commit操作之后,才能将数据真正写入。由于写入的数据在多数情况不能占满一个完整的Cache Block,此时需要和Probe操作带回的数据进行Merge后进行写入操作。
写操作的实现细节比上文描述的过程复杂得多。即便我们抛离了与Store Ordering相关的诸多和Memory Consistency的概念和各类Cache Write策略,没有考虑在Cache Write Hit时使用的Write-Through,Write-Back和Write-Once策略,没有考虑Cache Write Miss时使用的Write-Allocate, Write-Validate和Write-Around策略。
我们首先考虑在进行Write Probe操作之前,在Cache流水线准备好一个未使用的Cache Block,到从存储器子系统获得数据后真正写入数据的这段延时。我们首先关注这个刚刚准备好的Cache Block在此时使用的状态信息,似乎MOESIF这几个状态都无法准确描述此时的这条Cache Block所处的状态。
MOESIF这些状态位仅是Cache Controller所使用的简单得不能再简单的状态位,仅是一个Stable状态,是Cache Block诸多状态中一个子集。一个Cache Block中还含有其他状态位,更为复杂的处理Race Condition的Transient状态位。在Cache与Cache间的总线中包含着诸多Coherence Message和各类Bus Transaction。如果进一步考虑多级Cache的组成结构后,Cache Controller FSM使用的状态位和流程迁移的复杂程度令人叹为观止。
其次我们需要考虑存储器写操作在等待最终Commitment时所带来的延时。如上文所述,如果存储器读指令的访问结构在LSQ中命中时,微架构将不会读取L1或者更高层次的Cache,从而充分利用了这个延时。这个延时可以带来的另外一个好处,由于后续的存储器读指令可能需要读取相同的Cache Block,此时也要进行Probe Cache操作。在这个延时中,这两个Probe操作可以合并,从而在一定程度上降低了总线的Traffic。这些优化手段提高了存储器读写指令的效率,也带来了较大的系统复杂度。
在Opteron LS1中的存储器读或者写操作,如果没有及时完成,例如存储器写指令没有及时地进行Commitment,将导致存储器写操作无法在LS1中完成,此外如果存储器读没有在L1 Cache中命中也无法及时完成时,这些在LS1中的指令都将进入LS2,从而为后续的存储器指令预留空间。这些预留可以为更多可能在L1 Cache Hit的存储器操作并发执行。从这个角度分析可以发现LS1主要用于Cache Hit的处理。
在LS2中的所有存储器读写指令,包括Load,Store和Load-Store指令将继续监听Cache Probe的结果,当发生Cache Miss时,需要将存储器读写请求转发给BUI(Bus Interface Unit)部件,BUI部件将根据需要从L2 Cache或者主存储器中获得最终数据。即LS2用于处理Cache Miss时,需要较长时间的存储器读写指令。
在Opteron微架构中,采用了Non-Blocking Cache的实现方式,为每一条Miss存储器请求添加了一个MAB Tag(Missed Address Buffer Tag)之后再发送给BUI部件,同时为这些Miss请求在LS2中设置了Fill Tag。当Probe操作的数据从L2 Cache Controller返回时,其MAB Tag将与在LS2中的Fill Tag进行比较,进一步确认数据是否有效,并将LS2中的存储器指令的状态从Miss更新为Hit。
Store指令将在LS2中停留更长的时间,直到该指令从流水线中按序退出后,才能将数据写入到存储器子系统中。当发生Misprediction或者Exception,微架构可以丢弃在LS2中的暂存的Store指令。只要Store指令没有离开LSU,都可以丢弃,当然这种丢弃是条件的,并不是随意丢弃。
在Opteron微架构中,L1和L2 Cache Controller将与LSU共同完成一次存储器读写操作。在L1和L2 Controller中含有各类Buffer,和连接这些Buffer的通路。一次存储器访问指令,在通过指令流水后,将首先到达LSU,并由LSU将其请求转发至L1和L2 Cache Controller,并由这些Cache Controller完成剩余的工作。
Cache Controller需要在保证Memory Consistency的前提下,将数据重新传递给LSU,完成一次存储器读写的全过程。在一个微架构中,Cache Controller是一个较为复杂的功能,由其管理的Cache流水线是整个微架构的精华。
[1] 许多论文书籍认为F(reg, reg)指令不属于存储器指令。
4.4 Cache Controller的基本组成部件
在一个处理器系统中,存储器子系统是一个被动部件,由来自处理器的存储器读写指令和外部设备发起的DMA操作触发。虽然在存储器子系统中并无易事,DMA操作依然相对较易处理。在多数设计中,一个设备的DMA操作最先看到的是LLC,之后在于其他Cache进行一致性操作。通常处理器系统的LLC控制器将首先处理这些DMA操作。
随着通用处理器集成了更多的智能外部设备,这些智能设备已经直接参与到处理器系统的Cache Coherency中,这些设备可以直接访问原来属于处理器系统的存储器子系统。最典型的设备就是Graphic Controller。从Sandy Bridge微架构开始,x86处理器内部集成了Graphic Controller,Graphic Controller可以与处理器共享存储器子系统。这使得NVDIA无法继续参与这个内部集成,以获得许多显而易见的优点时,义无反顾地投奔ARM阵营。
本篇不再关注这些与外部设备相关的Cache一致性,也不再讲述与DMA操作相关的细节,重点关注来自指令流水线的存储器指令的执行,以及在这些存储器指令的执行过程中,通过Cache Controller时所进行的对应操作。
从实现的角度上看,任何一个复杂的处理器系统都是由数据缓冲,连接这些数据缓冲的通路和控制逻辑组成。其中与Cache Controller相关的通路,数据缓冲和控制逻辑是最重要的组成部件,也是处理器系统的数据通路的主体。
这些Cache Controller及其数据缓冲通过横向和纵向连接,组成一个基本的CMP系统,而后这些CMP通过各类拓扑结构进一步连接,形成复杂的ccNUMA处理器系统,如图4‑1所示。本篇所重点关注的是,在一个CMP处理器系统之内的Cache Controller组成与结构,并简单介绍ccNUMA处理器系统的互连方式。
在一个ccNUMA处理器系统中,Cache Controller由FLC/MLCs/LLC Cache Controller,DMA Controller和Directory Controller,共同组成。其中FLC Cache Controller与LSU和指令流水线直接相连,管理第1级分离的指令与数据Cache;MLCs Cache Controller可能由多级MLC Controller组成,上接FLC下接LLC Controller,管理中间层次的Cache。在多数CMP处理器中,FLC与MLCs Controller在一个CPU Core之内,属于私有Cache,并且与CMP处理器中的其他处理器的FLC/MLCs保持一致。
LLC Controller管理CMP处理器的最后一级Cache。在一个CMP处理器中,LLC通常由多个CPU Core共享,其组成结构可以是集中共享,也可以分解为多个Distributed LLC。Directory Controller是实现ccNUMA结构的重要环节,该Controller通常设置在Home Agent/Node中,并与主存储器控制器直接关联在一起。当一个数据请求在LLC中Miss后,将到达Home Agent,之后在进行较为复杂的CMP间的Cache Coherency/Memory Consistency处理。
我们首先讨论在一个CMP系统内,FLC,MLCs,LLC的连接拓扑结构,即Cache通路互连结构。在这个通路设计中,需要重点关注的依然是Throughput和Latency,同时实现处理器系统所约定的Memory Consistency模型。
在一个CPU Core内,FLC大多为Private Cache,当然我们需要忽略SMT这类典型的共享FLC的拓扑结构。MLCs可以被多个CPU Core Share,也可以为Private。如在Intel的Nehalem微架构[12]和Sandy Bridge微架构[69]中,MLCs为Private。最初Power系列处理器对多CPU Core共享MLCs情有独钟,Power4和Power5都采用了这种架构[75][76],而后出现的Power6和Power7放弃了Share MLCs这种方式,也采用了Private MLCs的方式[77][78]。
AMD最新的Bulldozer微架构采用了两个CPU Core共享一个MLC(L2 Cache)的方式,而且出人意料的使用了NI/NE (Non-inclusive and Non-Exclusive)和Write-Through组成结构[74],与之前微架构Cache的组成方式相比,唯一坚持的只有VIPT。
在有些CMP中,LLC为多个Core共享,也有部分CMP将LLC作为Victim Cache,如Power6和Power7[77][78]。为增加LLC的带宽和Scalability,一些CMP采用了Distributed LLC方式。我们首先讲述CPU Core与LLC之间的连接关系。在一个CMP系统中,多个CPU Core与LLC通常使用Share Bus,Ring或者Crossbar三种方式进行连接,如图4‑5所示。
其中Share Bus结构最为直观,在处理Memory Consistency时也最为便捷,所存在的问题也显而易见,即Share Bus所能提供的最大带宽有限,很难挂接更多的CPU Core。多个CPU Core在争用同一条总线时,不仅降低了有效带宽,也进一步加大了Latency。
目前许多CMP处理器使用了Ring Bus的组成结构,如Intel的Sandy Bridge[69],IBM的Power4[75],Power5[76],Cell处理器[79]和XLP832[80]。与Share Bus相比,Ring Bus能够提供的带宽相对较大,处理Memory Consistency也相对较为容易。
当使用Ring Bus时,每一个CPU Core通过Ring Node与Ring Bus互连,而每一个Ring Node与其相邻的两个Node采用点到点的连接方式。在一个实现中Ring Bus可以是单向的,也可以是双向的。由于Coherency的要求,Ring Bus通常由多个Sub-Ring组成,分别处理数据报文与Coherent Message报文。其设计难点主要集中在Ring Bus的Ordering处理和Ring-Based的Token Coherence模型。在某种程度上说,Share Bus与Ring Bus是一致的,尤其是在Snoop Message的处理器中。
与Share Bus和Ring Bus两种连接方式相比,使用Crossbar Switch方式可以提供较大的物理带宽,而且可以获得较小的Latency,然而在Memory Consistency层面需要付出更大的努力。在有些处理器中,联合使用了Crossbar和Ring结构。如Power4使用Crossbar连接两个L1 Cache和L2 Cache,而使用Ring连接L2 Cache,L3 Cache,L3 Directory和存储器控制器[75]。采用Crossbar Switch的一个重要的微架构是UltraSPARC系列处理器,目前UltraSPARC T1和T2(Niagara和Niagara 2)[81]采用了这种结构。在Niagara 2处理器中含有8个CPU Core,每一个Core中含有8个Thread。
T2微架构的Crossbar采用Non-Blocking,Pipelined结构,上接8个L1 Cache,下与8个Bank的L2 Cache相连,可以同时处理8个Load/Store数据请求和8个Data Return请求。L1 Cache采用Write-Through方式,L2 Cache采用Write-Back,Write-Allocate方式。使用Crossbar方式并不易处理Cache Coherent, T2微架构专门设置了L2 Directory[81],占用了较多的Die Size,以至于T2包括之后的SPARC64 VIIIfx(Venus) L3 Cache的容量较小,这对BLAS和其他用于科学计算的实现并没有太大影响。采用Venus微架构的K Computer集成了68,544个CPU Core,LINPACK的最终Benchmark结果达到了8.162 petaflops,跃居TOP500处理器之首[82]。
这些CMP处理器可以进一步通过互连网络,组成更为复杂的ccNUMA处理器系统,更为复杂的Supercomputer处理器系统,如K Computer,Tianhe-1A,Jaguar和Roadrunner等。此时的连接通路已在处理器芯片之外,这是Infiniband,QPI和HT这些连接方式的用武之地。连接上万个PE(Processor Element)的Interconnection令人惊叹。这些Interconnection所使用的数据通路,通信协议和状态机组成了一个夺目的奇观。
对Performance,Performance,Performance的渴望使得Supercomputer的设计无所不用其极。每一个子设计都异常重要,任何一个疏忽都会极大降低一个系统整体的Stability。而对Scalability的无限追求更加增大了系统的设计复杂度。这一切极大降低了Supercomputer的Programmability。
每念及此都会放弃画出使用几个CMP,组成2S,4S和8S系统的连接通路图,这张连接通路图甚至没有一个CMP内部使用的Cache Hierarchy的连接关系复杂。而且这些连接通路仅是Cache Hierarchy设计的一部分,在FLC,MLCs和LLC之间还存在各类的Buffer。经过多番考量,我决定首先介绍LSU,FLC与MLCs之间存在的Buffer,如图4‑6所示。
这张图依然不能反映CPU与其下Cache Hierarchy间的关系,一个实际的CPU Core与其下的存储器子系统间的连接异常复杂。不同的处理器架构其存储器子系统的实现也有较大的差别。但是对于一个存储器子系统而言,其所担负的主要任务依然明晰。
存储器子系统的首要任务是将所访问的数据经由各级Cache,最后传递到距离CPU Core最近的一级缓冲,即进行数据传送;另外一个任务是使用合适的机制管理与这些数据相关的状态信息,包括Cache的Tag,MSHR和其他复杂状态信息;最后可能也是需要额外关注的是,存储器子系统需要考虑本系统所使用的Consistency Model和Coherence Protocol。
在一个存储器子系统中,依然存在若干级子系统。其中每一个子系统大体由数据单元包括Data Array和相关Buffer,Cache控制器和连接通路这三大部分组成。这个子系统与其上和其下的子系统通过各类Buffer进行连接,协调有序地完成存储器子系统的三大任务。
上文已经多次提及LSQ和MSHR,而图4‑6中的RSHR(Request Status Handling Register)与MSHR并不等同,MSHR的主要功能是处理来自CPU Core访问引发的Cache Miss请求,而RSHR主要处理来自存储器控制器的Coherence请求。在有些微架构中并没有设置RSHR,而采用了其他类似的部件实现。
Fill Buffer和MSHR协调工作暂存来自其下Cache Hierarchy的数据,在其中可能只保存了部分Cache Block;Store through Queue主要用于Defer Write和Write Combining请求。Writeback Buffer暂存Evicted Cache Line,其主要目的是Defer Writeback这个总线Transaction,保证其下的Cache Hierarchy可以优先处理Cache Miss请求。
上文提及的Buffer绝非FLC/MLCs Controller使用的全部Buffer,还有许多Buffer与Cache的预读,Cache Block的替换状态,以及Cache Read/Write/Evict Policy相关。不同的微架构采用了不同的实现策略。在这些不同的实现策略中所关注的重点依然是Bandwidth,Latency和Consistency。
为实现以上任务,Cache Controller需要处理几大类数据请求和消息。首先是Data Transfer请求,即进行数据块的搬移;其次是Data Transfer Replies,这个Reply报文可以带数据如存储器读请求使用的应答报文,也可以不带数据;之后是State Inquiry/Update Requests和State Inquiry/Update Replies,该类报文为保证数据缓冲间的状态信息的Consistency。
在一个实际的处理器系统中,进一步考虑到多级Cache Controller和外部设备,这几大类数据请求与消息会进一步分为更多的子类,以维护Cache协议与状态机的正常运行。不同的处理器系统使用了不同的Cache协议与状态机,使用了不同的组成结构,进一步加大了Cache Controller的设计难度。在ccNUMA处理器系统中,包含许多与Coherence相关的Data和Message。这些内容与Cache Memory的控制逻辑和协议状态机相关。
另外一个与控制逻辑和协议状态机直接相关的是Cache Block状态,MOESIF只是其中的部分状态,还有许多用于Cache层次结构互连,用于Consistency层面的Base状态,还有一些Transient状态用于处理Cache Block状态切换时出现的Race Condition条件。
在不同的Cache Controller中,如FLC/MLCs/LLC和Directory Cache Controller,这些状态即便名称类似,其定义依然有所差异。这些状态位之间相互关联也相互影响,进一步提高了Cache Controller的设计复杂度。
我们暂时忽略Cache Controller使用的其他状态位,重点关注其中一个较为特殊的状态位,Inclusion Property[83]。Inclusion Property的发明者Wen-Hann Wang先生最后加入了Intel,影响了Intel从P6直到Sandy Bridge微架构的多级Cache Hierarchy设计。Wen-Hann先生的这一发明使得多级Cache Hierarchy间的组成结构除了Inclusive,Exclusive之外,多了另外一个选择NI/NE结构。
4.5 To be inclusive or not to be 1
无数经典的体系结构书籍专注于介绍Inclusive。这使得我所接触的毕业生和工程师很少有Exclusive和NI/NE Cache的概念,包括几年前的自己。一些甚至是来自处理器厂商的工程师也对此知之甚少。也许我们早已熟悉了Inclusice这种Cache Hierarchy结构,认为CPU Core访问L1 Cache中Miss后查找L2 Cache,L2 Cache Miss后继续查找其下的层次。而现代CMP处理器在多级Cache的设计中更多的使用着Exclusive或者NI/NE的结构,纯粹的Inclusive结构在具有3级或者以上的Cache层次中并不多见。
在本节中,我们引入Inner和Outer Cache的概念。Inner Cache是指在微架构之内的Cache,如Sandy Bridge微架构中含有L1和L2两级Cache,而Outer Cache指在微架构之外的Cache,如LLC。在有些简单的微架构中,Inner Cache只有一级,即L1 Cache,Outer Cache通常由多个微架构共享,多数情况下也仅有一级。
在有些处理器系统中,Inner Cache由多个层次组成,如Sandy Bridge微架构中,包括L1和L2 Cache,这两级Cache都属于私有Cache,即Inner Cache。此时Inclusive和Exclusive概念首先出现在Inner Cache中,之后才是Inner Cache和Outer Cache之间的联系。为便于理解,如果本节约定Inner Cache和Outer Cache只有一级。
Inclusive Cache的概念最为直观,也最容易理解,采用这种结构时,Inner Cache是Outer Cache的一个子集,在Inner Cache中出现的Cache Block在Outer Cache中一定具有副本;采用Exclusive Cache结构时,在Inner Cache中出现的Cache Block在Outer Cache中一定没有副本;NI/NE是一种折中方式,Inner Cache和Outer Cache间没有直接关联,但是在实现时需要设置一些特殊的状态位表明各自的状态。
Inclusive Cache层次结构较为明晰,并在单CPU Core的环境下得到了广泛的应用。但是在多核环境中,由于Inner Cache和Outer Cache间存在的Inclusive关联,使得采用多级Cache层次结构的处理器很难再使用这种方式。如果在一个处理器系统中,每一级Cache都要包含其上Cache的副本,不仅是一种空间浪费,更增加了Cache Coherency的复杂度。
即便只考虑2级Cache结构,严格的Inclusive也不容易实现。为简化起见,我们假设在一个CMP中含有两个CPU Core。在每一个Core中,L1为Inner Cache,而L2为Outer Cache,Inner Cache和Outer Cache使用Write-Back策略,Core间使用MESI协议进行一致性处理。
我们首先讨论L1 Cache Block,在这个Cache Block中至少需要设置Modified,Exclusive,Shared和Invalid这4种Stable状态。由于Strict Inclusive的原因,还有部分负担留给了L2 Cache Block。L2 Cache Block除了MESI这些状态位之外,为了保持和L1 Cache之间的Inclusive,还需要一些额外的状态,如Shared with L1 Cache,Owned by L2 Cache,L1 Modified and L2 stale等一些与L1 Cache相关的状态。
如果在一个CMP中,仅含有两级Cache,增加的状态位仍在可接受的范围内,但是如果考虑到L3 Cache的存在,L3 Cache除了自身需要的状态之外,还受到Inclusive的L1和L2 Cache的影响,与两级Cache相比,将出现更多的组合,会出现诸如L1 Modified,L2 Unchanged and Shared with L3这样的复杂组合。
有人质疑采用Inclusive结构,浪费了过多的Cache资源,因为在上级中存在的数据在其下具有副本,从而浪费了一些空间。空间浪费与设计复杂度间是一个Trade-Off,在某些场景之下需要重点关注空间浪费,有的需要关注设计复杂度。
通常只有Outer Cache的容量小于Inner Cache的4倍或者8倍时,空间浪费的因素才会彰显,此时采用Exclusive Cache几乎是不二的选择。而当Outer Cache较大时,采用Inclusive Cache不仅设计复杂度降低,这个Inclusive Outer Cache还可以作为Snoop Filer,极大降低了进行Cache Coherency时,对Inner Cache的数据访问干扰。
采用Inclusive Cache的另一个优点是可以将在Inner Cache中的未经改写的Cache Block直接Silent Eviction。这些是Inclusive Cache的优点。但是从设计的角度上看,采用Inclusive Cache带给工程师最大的困惑是严格的Inclusive导致的Cache Hierarchy间的紧耦合,这些耦合提高了Cache层次结构的复杂度。
首先考虑Outer Cache的Eviction过程,由于Inner Cache可能含有数据副本,因此也需要进行同步处理。采用Backward Invalidation可以简单地解决这个问题,由于Snoop Filter的存在,使得这一操作更加准确。不过我们依然要考虑很多细节问题,假如在Inner Cache中的副本是已改写过的,在Invalidation前需要进行数据回写。
另外一个需要重点关注的是在Inclusive Cache设计中存在的Race Condition。无论是采用何种Cache结构,这些临界条件都需要进行特别处理,但是在Inclusive Cache中,Inner Cache与Outer Cache的深度耦合加大了这些Race Condition的解决难度。
我们首先考虑几种典型的Race Condition。假设Outer Cache由于一个CPU Core存储器操作的Cache Miss而需要进行Outer Cache的Eviction操作。由于Inclusive的原因,此时需要Backward Invalidate其他CPU Core Inner Cache Block,而在此同时,这个CPU Core正在改写同一个Cache Block。同理假设一个CPU Core正在改写一个Inner Cache Block,而另外一个CPU Core正在从Outer Cache Block中读取同样的数据,也将出现Race Condition。
这些Race Condition并不是不可处理,我们可以构造出很多模型解决这些问题。这些模型实现的相同点是在Inner Cache和Outer Cache的总线操作间设置一些同步点,并在Cache Block中设置一些辅助状态,这些方法加大了Cache Hierarchy协议与状态机的实现难度。解决这些单个Race Condition问题,也许并不困难,只是诸多问题的叠加产生了压垮骆驼的最后一根稻草。如果进一步考虑CMP间Cache的一致性将是系统架构师的噩梦。
这并不是Inclusive Cache带来的最大问题。这个噩梦同样出现在Exclusive和NI/NE结构的Cache层次结构设计中,只有每天都在做噩梦还是两天做一个的区别。顶级产品间的较量无他,只是诸多才智之士的舍命相搏。
Inclusive Cache绝非一无是处,如上文提及的Snoop Filter,Inclusive LLC是一个天然的Snoop Filter,因为在这类LLC中拥有这个CMP中所有Cache的数据副本,在实现中只需要在LLC中加入一组状态字段即可实现这个Filter,不需要额外资源,Nehalem微架构使用了这种实现方式[12]。
在电源管理领域,使用纯粹的Inclusive Cache时,由于Outer Cache含有Inner Cache的全部信息,因此在CPU Core进入节电状态时,Inner Cache几乎可以全部进入低功耗状态,仅仅维护状态信息,不需要对其内部进行Snoop操作。这是采用NI/NE和Exclusive Cache无法具备的特性。
采用Exclusive Cache是Pure Inclusive Cache的另一个极端,从设计实现的角度上看,依然是紧耦合结构。在这种Cache组成结构中,1个Cache Block可以存在于Inner Cache也可以存在于Outer Cache中,但是不能同时存在于这两种Cache之中。与Inclusive Cache相比,Inner和Outer Cache之间避免了Cache Block重叠而产生的浪费,从CPU Core的角度上看,采用Exclusive Cache结构相当于提供了一个容量更大的Cache,在某种程度上提高了Cache Hierarchy的整体Hit Ratio。
在一个设计实现中,Cache的Hit Ratio和许多因素相关。首先是程序员如何充分发挥任务的Temporal Locality和Spatial Locality,这与微架构的设计没有直接联系。而无论采用何种实现方式,对于同一个应用,提供容量更大的Cache,有助于提高Cache的Hit Ratio。
由上文的讨论我们可以轻易发现,在使用相同的资源的前提下,使用Exclusive Cache结构可以获得更大的Cache容量,此时Cache的有效容量是Inner Cache+Outer Cache。这是采用Inclusive Cache或者NI/NE无法做到的,也是Exclusive Cache结构的最大优点。这仅是事实的一部分。
假设在一个微架构中,含有两级Cache,分别是Inner和Outer,并采用Exclusive结构。此时一次存储器读请求在Inner Miss且在Outer Hit时,在Inner和Outer中的Cache Block将相互交换,即Inner Cache中Evict的Cache Block占用Outer Cache Hit的Cache Block,而Outer Cache Hit的Cache Block将占用Inner Cache Evict的Cache Block。
如果存储器读请求在Inner和Outer Cache中全部Miss时,来自其下Memory Hierarchy的数据将直接进入Inner Cache。因为Exclusive的原因,来自其下Memory Hierarchy的数据不会同时进入到Outer Cache。
在这个过程中,Inner Cache可能会发生Cache Block的Eviction操作,此时Eviction的Cache Block由Outer Cache接收,此时Outer Cache也可能会继续出现Eviction操作。这些因为Inner或者Outer Cache的Eviction操作而淘汰的Cache Block,也被称为Victim Cache Block或者Victim Cache Line。
在采用Exclusive Cache的微架构中,需要首先考虑Victim Cache Block的处理。当CPU Core进行读操作时,如果在Inner Cache中Miss,需要从Outer Cache或者其下的Memory Hierarchy中获得数据,这个数据直接进入到Inner Cache。此时Inner Cache需要首先进行Eviction操作,将某个Cache Block淘汰。这个Victim Cache Block需要填充到某个数据缓冲中。可以是Outer Cache作为Victim Cache。即淘汰的Cache Block进入Outer Cache,当然采用这种方法,可能继续引发Outer Cache的Eviction操作,从而导致连锁反应。
在采用Exclusive Cache结构的处理器系统中,Outer Cache经常Hit的Cache Block也是Inner Cache经常Evict的Cache Block[84]。这与Wen-Hann有关NI/NE Cache结构的Accidentally Inclusive的结论一致,在NI/NE Cache结构中,虽然Inner Cache与Outer Cache彼此独立工作,但是根据统计在多数时间,在Inner Cache中Hit的Cache Block也存在于Outer Cache。这不是设计的需要,而是一个Accident,Wen-Hann将其称为Accidentally Hit[85]。
[84]和[85]的结果是对同一现象的两个不同角度的观察,这一现象由Inner Cache和Outer Cache的相互关联引发。对于使用Exclusive Cache结构,需要使用某类缓冲存放淘汰的Cache Block,如Victim Replication[88],Adaptive Selective Replication[84]等一系列方式,当然理论界还有更多的奇思妙想。这些内容进入到了比较专业的领域,并不是本篇的重点,本篇仅介绍AMD K7系列的Athlon和Duron微架构的实现方式。
4.6 To be inclusive or not to be 2
在Athlon微架构中,L1 Cache的大小为128KB,分别为64KB Data和64KB Instruction Cache,运行频率与CPU Core Clock相同,在Hit时的Load-Use Latency为3个Clock Cycle。L2 Cache大小为512KB,运行频率为CPU Core Clock的一半[86][1]。L1与L2 Cache之间的比值为4,这使得Exclusive Cache结构成为必然的选择。在L1 Cache与L2 Cache之间,Athlon微架构设置了专用的Buffer,暂存从L1 Cache中淘汰的Cache Block,这个Buffer也被称之为Victim Buffer。L1 Cache,L2 Cache与Victim Buffer的组成结构如所示。
在Athlon微架构中,Victim Buffer由8个64B大小的Entry组成。而L1 Cache,Victim Buffer和L2 Cache之间也是严格的Exclusive关系。在Athlon微架构盛行的年代,L1 Cache容量为128KB是一个很大的数字,这个数字放到今天也并不小。这使得CPU Core访问的多数数据在L1 Cache中命中,L1 Cache和L2 Cache间的总线并不繁忙,Victim Buffer暂存的Cache Block可以在总线Idle时与L2 Cache进行同步。Victim Buffer很少会因为所有都Entry被占用而成为系统瓶颈[86]。
当CPU Core读取的数据在L1 Cache Miss,而在Victim Buffer Hit时,数据将从Victim Buffer中传递给CPU Core和L1 Cache,从L1 Cache中Evict的Cache Block将送至Victim Cache,无需L2 Cache的参与。即便数据访问在L1 Cache和Victim Buffer全部Miss时,Athlon微架构L2 Cache的Load-Use Latency也仅为11个Clock Cycle,包括L1 Miss所使用的3个Cycle。
在Victim Buffer为满,CPU Core访问的数据在L1 Cache中Miss且在L2 Cache中Hit的场景中,Victim Buffer暂存的Cache Block需要使用8个Cycle刷新到L2 Cache中;之后L2 Cache需要2个Cycles的Turnaround周期将命中的Cache Block提交给L1 Cache,同时将在L1 Cache中Evict的Cache Block送往Victim Buffer;最后L2 Cache还需要2个额外的Turnaround周期完成整个操作。此时L2 Cache的Load-Use Latency也仅为20个Cycle。
Athlon微架构的Cache Hierarchy结构在与当时同类处理器的较量中赢得了先机。而在多数应用场景中,微架构间的较量首先发生在Cache Hierarchy中。而后的K8微架构进一步优化了Cache Hierarchy结构。
AMD在Magny Cours微架构中,L3 Cache作为L2 Cache的Victim[87]。AMD对Exclusive Cache情有独钟,基于K7,K8和K10的一系列微架构均使用了这一结构。在当时AMD凭借着Cache Hierarchy结构上这些貌似微弱的领先优势,迎来了其历史上风光无限的时代。Intel直到Nehalem微架构之后才真正超越了AMD,并开始在Cache Hierarchy领域上的一骑绝尘。
AMD最新的Bulldozer微架构在Cache Hierarchy层面上做出了较为激进的改革,虽然在L1 Cache层面依然使用VIPT方式,大小为16KB,却也不再坚持之前微架构3个Cycle的Load-to-use Latency,而是扩大到4个Cycle。Latency的提高使得Bulldozer在L1 Cache层面使用128位总线带宽成为可能,提高了总线的带宽。
L2 Cache由两个CPU Core共享,最大可达2MB,这使得Exclusive Cache的组成方式失去意义,不出意外Bulldozer微架构采用了NI/NE方式。最令人意外的是该微架构L2 Cache的Load-to-use Latency达到了18~20个Cycle,远高于Nehalem和Sandy Bridge微架构的10个Cycle[74][12][69]。但是与Nehalem和Sandy Bridge相比,Bulldozer微架构提高了可并发的Outstanding Cache Miss总线请求, L1 Cache Miss的可并发总线请求为8个,L2 Cache Miss的可并发总线请求为23个。
Bulldozer微架构这些设计必然经过详尽的Qualitative Research和Quantitative Analysis,在没有获得精确的性能数据之前,无法进一步诠释Bulldozer微架构的优劣。事实上即便你拿到这些数据,又能够说明多少问题。这些数据很难真正地做到公正全面,在很多情况之下依然是一个片面的结果。此时可以肯定的是,AMD依然不断前进摸索,在工艺落后Intel的事实中正在寻求新的变化,这个公司值得尊敬。
Bulldozer微架构放弃了Exclusive Cache结构必定基于其深层次的考虑。即便是通过简单的理论分析,Exclusive Cache也并非完美。Exclusive Cache是Pure Inclusive Cache的另一个极端表现方式,Inner Cache与Outer Cache间依然紧耦合联系在一起,这造成了各级Cache间频繁的数据交换,尤其是Inner Cache和Victim Cache之间的数据颠簸。在CMP组成的ccNUMA处理器系统中这种颠簸更加凸显。
首先在Outer Cache作为Inner Cache的Victim时,Outer Cache仍需要监控发向Inner Cache的Request和Reply等信息,加大了Outer Cache Controller的设计难度。其次在一个CMP处理器系统中,某个CPU Core发起的Snooping操作,必须要同时Probe其他CPU Core的Outer Cache Tag和Inner Cache Tag。
对Inner Cache Tag的Probe操作必将影响当前CPU Core对Inner Cache的访问延时,而这个延时恰是单个CPU Core设计所重点关注的内容,处于关键路径。如果每次Probe操作都直接访问L1 Cache的Tag,将影响CPU Core对L1 Cache的访问,可能会Stall指令流水线的执行,带来严厉的系统惩罚。
通常情况下,处理器可以使用两种方法解决这类问题。一个是设置专用的Snoop Filter,处理来自其他CPU的Snoop Transaction,减少对Inner Cache不必要的Probe,对于Exclusive Cache设置Snoop Filter需要额外的逻辑,而Inclusive Cache较易实现Snoop Filter。另一种方法是复制Inner Cache的Tag,实现CPU Core访问Inner Cache Tag与Snoop的并行操作。
这两种方法都会带来额外的硬件开销,从而加大了Cache Controller的设计难度。不仅如此,在Cache Hierarchy的设计中每加入一个Buffer都要细致考虑Memory Consistency层面的问题,各类复杂的Race Condition处理和由此带来的Transient State。这一切使本身已经极为复杂的Cache Controller,更加难以设计。
NI/NE Cache是Exclusive Cache与Inclusive Cache的折衷。Intel从P6处理器开始一直到目前最新的Sandy Bridge处理器,一直使用这种结构。Intel也曾经尝试过Exclusive和Inclusive Cache结构,最终坚持选择了NI/NE结构,也开始了Intel x86在Memory Hierarchy领域的领先。但是我们不能依此得出NI/NE结构是最优的结构,也不能认为这个结构是一个很古老的设计而应该淘汰,在没有得到较为全面的量化结果之前,很难做出孰优孰劣的判断。即便有这些结果也不能贸然作出结论。
在使用NI/NE Cache结构时,Inner Cache与Outer Cache的部分将内容重叠,与Inclusive结构相比,Cache容量利用率相对较高,但是仍然不及Exclusive Cache结构,因为Accidentally Hit的原因,NI/NE Cache容量利用率与Inclusive Cache相比,提高得较为有限。单纯从这个角度出现,在设计中并没有使用NI/NE Cache结构的强大动力。
此外采用NI/NE Cache方式时,在Outer Cache中不保证包含Inner Cache中的全部信息,因此其他CPU Core的Snoop Transaction仍然需要Probe Inner Cache,这使得NI/NE Cache方式依然要复制Inner Cache Tag,或者加入一个Snoop Filer。
从以上两方面分析,我们很难得出使用NI/NE结构的优点。NI/NE Cache的支持者显然会反对这些说法,他们会提出很多NI/NE Cache的优点。NI/NE Cache可以在Outer Cache中加入IB(Inclusive Bit)和CD(Clean/Dirty)位,即可克服Inclusive和Exclusive Cache存在的诸多缺点,并带来许多优点,如消减Outer Cache的Conflict Miss,充分利用Inner Cache与Outer Cache间总线带宽,Write Allocate on Inner Cache without Outer Cache interaction,减少不必要的RFO(Read for ownership)操作等。
我无从辩驳这些确实存在的优点,但是更加关心这些优点从何而来。从一个工程师的角度上看,NI/NE Cache带来的最大优点莫过于简化了Cache Hierarchy的设计。与使用Inclusive和Exclusive Cache结构相比,采用这种方式使得Inner Cache和Outer Cache间的耦合度得到了较大的降低,也因此降低了Cache Hierarchy的设计难度。
耦合度的降低有助于Inner和Outer Cache Controller设计团队在一定程度上的各自为政。这种各自为政的结果不仅仅提高了Cache Controller的效率,更重要的是提高了设计人员的工作效率。但是这种各自为政只是在一定程度上的,Inner是Outer Cache的Inner这个事实决定了Inner Cache和Outer Cache无论采用何种方式进行互联,依然存在大的耦合度。
NI/NE Cache结构并不是Intel x86处理器的全部。Intel近期发布的Nehalem和Sandy Bridge处理器,在使用NI/NE Cache的同时,也使用了Inclusive Cache。Nehalem EP处理器含有4个CPU Core,其中每一个CPU Core含有独立的L1和L2 Cache,其中L1和L2 Cache为Inner Cache;而所有CPU共享同一个L3 Cache,这个L3 Cache也被称为LLC或者Outer Cache。
其中L1 Cache由32KB的指令Cache与32KB的数据Cache组成,采用NI/NE结构;L2 Cache的大小为256KB,采用NI/NE结构;L3 Cache的大小为8MB,采用Inclusive结构,即该Cache中包含所有CPU Core L1和L2 Cache的数据副本。Inclusive LLC也是一个天然的Snoop filter。在LLC中的每一个Cache Block中都含有一个由4位组成的Valid Vector字段,用来表示LLC中包含的副本是否存在于4个CPU Core的Inner Cache中[12]。
当Valid Vector[i]为1时,表示第i个CPU Core的Inner Cache中可能含有LLC中的Cache Block副本,因为NI/NE结构的缘故,Valid Vector[i]为1并不保证Inner Cache中是L1还是L2 Cache中含有数据副本,仍然需要进一步的Probe操作;当Valid Vector[i]为0时表示,第i个CPU Core的Inner Cache中一定不含有LLC中的Cache Block副本[12]。
LLC的Valid Vector字段可以简化由Nehalem EP/EX处理器组成的ccNUMA处理器系统中的Cache Coherency。因为LLC可以代表一个Nehalem EP/EX处理器中的所有Cache,当其他Socket进行Snoop Cache时,仅需首先访问LLC即可,而不必每一次都需要Probe所有Inner和Outer Cache,从而简化了Cache Hierarchy的设计。
Sandy Bridge处理器的Snoop Filter的设计与Nehalem EP/EX处理器类似,只是进一步扩展了Nehalem EP/EX处理器的Valid Vector字段,以支持内部集成的GPU[69]。采用Exclusive Cache结构的Magny Cours在6MB的L3 Cache中划出了1MB的Probe Filter Directory作为Snoop filter[87],而且提高了Cache Controller的设计复杂度。
AMD在其工艺落后于Intel,在相同的Die Size只能容纳更少晶体管数目的劣势下,使用规模庞大Probe Filter Directory有利于多个CMP系统间Cache一致性的实现,尤其在4个或者更多Socket的场景。但是即便是在这个场景下,AMD仅依靠Probe Filter Directory并不足以超过Intel。Sandy Bridge在LLC层面的实现几乎独步天下,不仅运行在Clock Frequency,而且其Load-Use Latency仅为26~31个Cycles[69]。
除此之外Sandy Bridge在LLC的实现中使用了Distributed的方式,将一个LLC分解为多个Slice,其中每一个CPU Core对应一个Slice,CPU Core经过Hash结果访问各自的Slice。这种Partitioning Cache Slice降低了Cache Coherency的设计难度,进一步提高了LLC的总线带宽,提高了LLC的Scalability,避免了潜在的Cache Contention[69]。AMD的Magny Cours也支持这种Cache组织方式[87]。除此之外,NI/NE with Inclusive Cache还可以使用一些手段进一步优化,如[90]中介绍的TLA(Temporal Locality Aware)算法。
无论是Intel采用的NI/NE Inner Cache加Inclusive Outer Cache的结构,还是AMD采用的Exclusive Cache结构[3],在Cache Hierarchy的设计中,只有耦合程度相对较低的区别。如果从数字逻辑的设计角度上看,这些设计都是耦合的不能再耦合的设计。
在很多场景中,一个完美的设计通常从少数人开始,这也意味着设计的强耦合,一体化的设计有助于整体效率的提高,但也很容易扼杀子团队的创造热情。一个将完美作为习惯的架构师最终可以左右一个设计,创造出一个又一个属于他的完美设计。这样产生的完美,不可继承,不可复制,等待着粉碎后的重建。这样的完美本身是一场悲剧,这些悲剧使得这些完美愈显珍贵。
不同群体对完美的不同认知使得这些悲剧几乎发生在每朝每代。这些完美的人不可轻易复制,使得一个大型设计通常选用多数人基于这个时代赋予认知后的完美,属于多数人的完美。这使得架构师在设计产品时,若只考虑技术层面,而不考虑设计者间的联系,并不称职。这也使得一个优秀的架构师在历经时光痕迹,岁月沧桑后,做出了很多不情愿也不知对错的中庸选择。这些中庸可能是大智慧。而总有一些人愿意去挑战这些中庸。
[1] 本文介绍的Athlon处理器采用的L2 Cache为Full-Speed On-Die L2 Cache。
[2] 原图来自[89],并有所改动。笔者并不认同该论文的部分内容,仅引用该图。
[3] Magny Cours微架构并不是严格意义的Exclusive Cache,L3 Cache需要检查True Sharing状态[76]。
4.7 Beyond MOESIF 1
再次回顾与Cache Coherency相关的MOESIF状态位,却不知从何说起。MOESIF这些状态位似曾相识,已物是人非。在CMP处理器系统中使用的多级Cache层次结构和CMP间的Cache Coherency,改变了MOEIF这些状态位的原始形态。
在一个由多个CMP组成的ccNUMA处理器系统中,Cache Coherency包含两方面内容,首先是Intra-CMP Coherence,其次是Inter-CMP Coherence。其中Intra-CMP Coherence指一个CMP内部的Cache Coherency,而Inter-CMP Coherence指CMP间的Cache Coherency,两者之间的关系如图4‑8所示。
在一个ccNUMA处理器系统中,Intra-CMP Coherence需要与Inter-CMP Coherence协调工作,完成Cache的全局Coherency。不同的CMP处理器采用了不同的Intra-CMP Coherence策略,可以是Share Bus,Ring Bus或者Directory,这些策略各有利弊。Inter-CMP Coherence甚至可以通过软件交换Message的方式实现,而为了提高软件的Programmability,多数系统使用了硬件实现这些Message交换。在一个ccNUMA处理器系统中使用的CMP超过4个时,多使用Directory结构。
在串行总线替代并行总线的大趋势下,ccNUMA处理器的数据以及Coherence Message通过Packet的方式进行传递,会涉及在Internet中出现的Router,NI(Networking Interface),Flow Control,QoS,Routing Algorithm等概念。在ccNUMA处理器系统中使用的Interconnection不但不比Internet简单而且复杂得多。K Computer使用的6D Mesh/Torus Interconnect[92]结构目前尚无用于Internet的可能。
Supercomputer使用的Interconnect超出了本篇的讨论范围,但是仅从Cache的Coherency层面进行分析,Inter-CMP Coherence的设计没有难于Intra-CMP Coherence,Cache Coherency依然是越接近CPU Core越复杂,Cache间的互联总线也是越接近CPU Core越复杂。这是本节重点介绍Intra-CMP Coherence的主要原因。
Intra-CMP Coherence的复杂程度超过了初学者的想象。其中各级Cache之间的关系,及为了处理这些关系而使用的Cache Block状态和总线协议均较为复杂。仅是其中使用的Cache Coherency Protocol也复杂到了需要使用专门的语言才能将其简约地进行描述。这个语言即SLICC(Specification Language for Implementing Cache Coherence),这个语言是有志于深入了解Cache Coherency Protocol所需要了解的基础知识。
对于多数人而言,学习Cache Coherency Protocol最好的工具是Simulator和使用SLICC语言书写的这些源代码。虽然在模拟舱中很难学会开飞机,但是如果连模拟舱都没有呆过,很难有人让你开真飞机。学术领域提供了这样的模拟舱学习Cache Coherency Protocol。
与CMP处理器相关的模拟器主要有SESC,Simics,M5和GEMS。Simics最初由Virtutech开发。当时的Virtutech和飞思卡尔在多核处理器上进行了一些合作,虽然Simics是商业产品,我们当时却有机会获得无需付费的License。如获至宝。Intel后来收购了Virtutech。
M5和GEMS主要用于教学与科研,是一个免费而且代码公开的模拟器。M5侧重CPU Model,ISAs等方面;GEMS最重要的组成部件是Cache Coherency Protocol和Ruby Memory Hierarchy。M5和GEMS具有很强的互补性,也正是因为这个原因,这两个Simulator逐步融合为GEM5 Simulator[93]。
GEM5是我目光所及范围内,由脚本语言书写的最复杂的系统。GEM5吸纳了M5和GEMS的主要优点,支持Alpha,ARM,SPARC和x86处理器,支持Functional和Timing Simulation,提供FS(Full-System)和SE(Syscall Emulation)两种方式。即便是Android这样复杂的系统也可以运行在GEM5 Simulator之上。在GEM5 Simulator中包含许多内容,本节重点关注使用SLICC语言实现的Cache Coherence Protocol。
GEM5 Simulator的源代码可在http://www.gem5.org/Download中下载。在这些源代码中,我们重点关注./src/mem/protocol目录。这个目录包含GEM5支持的所有Cache Coherence Protocol,包括MI_example,MOESI_hammer,MOESI_CMP_token,MOESI_CMP_directory和MESI_CMP_directory,由.slicc和.sm两类文件组成。其中xyz.slicc文件包含实现xyz protocol所需要的所有.sm文件,而在.sm文件中包含各级Cache Controller的具体实现。
在每一个.sm文件中,首先包含一个machine(L1Cache, "MSI Directory L1 Cache CMP"),用以指出当前Cache Controller的名称和采用的协议。其后是由Network Ports,States,Events,Ruby Structure,Trigger Events,Actions和Transitions组成的基本模块。
- Network Ports通过MessageBuffer原语定义,描述Cache使用的“From”和“to”数据通路,使用的virtual_network编号等一系列信息。
- States指Cache Block使用的状态位,由Base和Transient States组成。这些状态与Cache Coherence Protocol相关,超越了常用的MOESIF这些状态位。多级Cache层次结构赋予了MOESIF这些基本状态位以新的表现形式。
- Events将引起Cache Block的状态迁移,包括Load,Ifetch,ACK,NAK等一系列事件。这些Event可以由外部也可以由内部产生。
- Ruby Structures用于定义当前程序内部使用的Variables,Structures和Functions,也可以引用当前程序之外的Ruby Structures。
- Trigger Events定义Input和Output端口。每一个端口将和一个MessageBuffer绑定。Input端口可以设置执行代码,当前Components Wakeup时,这段代码将首先检查端口中是否有Message,如果有则使用peek函数其存放到一个内部变量in_msg中,之后可以对这个in_msg进行分析,并做相应的操作,如Trigger某个Event。
- Actions描述Cache Block进行状态迁移时所需要进行的操作,如enqueue和dequeue操作等等。
- Transitions由Starting State,Triggering Event,Final State组成和一系列Actions组成。如果没有Final State参数,说明完成Actions后,将停留在Starting State。
对于某一个具体的Cache Coherence Protocol而言,这些.sm文件累加在一起也并不多。其中最复杂的MOESI_CMP_directory Protocol,其总代码也仅有5,565行。只是这些.sm文件相互关联,相互依托,需要首先理解的是Cache的数据通路,Cache间总线的各类Transaction,基本的Cache一致性模型,CPU的LSU部件和许多与体系结构相关的基础知识。
在掌握与Cache相关的基础知识之后,阅读并理解这些源代码不会成为太大的障碍。建议读者从最简单的MI_example Protocol开始直到较为复杂的MOESI_CMP_directory Protocol。下文将重点讨论GEM5的MOESI_CMP_directory protocol,我并不是要挑战GEM5中最难的Protocol,而是GEM5的网站提供了一些基本的State Transition的FSM(Finite-State Machine)转换图,省去了许多工作。
我们首先列出MOESI_CMP_directory protocol使用的Coherence Messages,这些Messages由两部分组成,一个是Coherence Request,另一部分是Coherence Response,其详细描述如表4-2与表4-3所示。
表4-2 MOESI_CMP_directory protocol使用的Coherence Request[94]
Message |
Description |
GETX |
Request for exclusive access. |
GETS |
Request for shared permissions to satisfy a CPU's load or IFetch. |
PUTX |
Request for writeback of cache block. |
PUTO |
Request for writeback of cache block in owned state. |
PUTO_SHARERS |
Request for writeback of cache block in owned state but other sharers of the block exist. |
PUTS |
Request for writeback of cache block in shared state. |
WB_ACK |
Positive writeback ack |
WB_ACK_DATA |
Positive writeback ack with data |
WB_NACK |
Negative writeback ack |
INV |
Invalidation request. This can be triggered by the coherence protocol itself, or by the next cache level/directory to enforce inclusion or to trigger a writeback for a DMA access so that the latest copy of data is obtained. |
DMA_READ |
DMA Read Request |
DMA_WRITE |
DMA Write Request |
在GETX,GETS,PUTO等往往包含一些前缀,其中L1_前缀指来自L1 Cache的请求,而Fwd_前缀指来自其他CMP的请求,而Own_前缀指来自当前CMP的请求。这些Request比较基本,需要认真理解。
表4-3 MOESI_CMP_directory protocol使用的Coherence Response[94]
Message |
Description |
ACK |
Acknowledgment, responder doesn't have a copy |
DATA |
Acknowledgment, responder has a data copy |
DATA_EXCLUSIVE |
Data, no processor has a copy |
UNBLOCK |
Message to unblock next cache level/directory for blocking protocols. |
UNBLOCK_EXCLUSIVE |
Unblock, we're in E/M |
WRITEBACK_CLEAN_DATA |
Clean writeback with data |
WRITEBACK_CLEAN_ACK |
Clean writeback without data |
WRITEBACK_DIRTY_DATA |
Dirty writeback with data |
DMA_ACK |
Ack that a DMA write completed |
不同的Cache Controller使用不同的Coherence Request和Response Message。对于L1 Cache其上的请求来自CPU Core,也被称为Sequencer,并可将请求发至L2 Controller,Response可以来自CPU Core也可以是L2 Controller。L1 Cache Controller还需要处理来自CPU Core的Load,Store和IFetch请求,除此之外其内部还可能产生L1_Replacement请求。
使局势更加错综复杂的是这些Request和Response使用了不同的Virtual Network,因为Request和Response之间的同步而产生的Race Condition并不容易难以解决。这也决定了在L1或者L2 Cache Block中并不是只有MOESIF这样简单的状态,每一个Cache Controller都使用着不同的状态位。这些状态位及其迁移组成了一个本不是采用FSM能够描述清楚的FSM。
在MOESI_CMP_directory Protocol的L1 Cache Controller中包含了7个Stable的状态位。
- I。当前Cache Block不含有有效数据。与传统定义相同。
- S。当前Cache Block用于1个或者多个数据副本,与传统定义相同。Read_Only。
- O。当前Cache Block含有有效数据,与传统意义的O状态位相同。Read_Only。
- M。仅是当前Cache Block含有有效数据,与传统意义的E状态位相同。Read_Only。
- M_W。仅是当前Cache Block含有有效数据,与传统意义的E状态位相同。Cache Block处于该状态时,禁止Replacement和DMA对其的访问,经过一段延时后,将自动转换为M状态。Store:Hit时,该状态将迁移到MM_W状态。Read_Only。
- MM。与传统意义的M状态位相同。Read_Write。
- MM_W。与传统意义的M状态位相同。Cache Block处于该状态时,禁止Replacement和DMA对其的访问,经过一段延时后,将自动转换为MM状态。Read_Write。
由这些状态组成的FSM如图4‑10所示。
值得留意的是MM_W到MM,M_W到M_W状态的迁移。MM_W状态由Store:GETX和Store:Hit触发,由S,I,O和M_W状态迁移而来,为减少DMA操作对Cache状态机的影响,同时为了避免很快替换最近改写的Cache Block,FSM要求最近改写的Cache Block在MM_W状态停留一段时间后,才能进入MM状态。在M_W设置的Timeout是同一个道理。
在MOESI_CMP_directory Protocol中,L2 Cache Controller的设计难度更加复杂,因为该Controller需要兼顾L1 Cache Controller和其下的Directory Controller。L2 Cache Controller共设置了14个Stable状态,处理CMP_directory Protocol。这些状态共分为4组,其含义与简要说明如表4-4所示。
表4-4 L2 Cache Controller使用的Stable状态[95]
Intra-Chip Inclusion |
Inter-Chip Exclusion |
State |
Description |
Not in any L1 or L2(1) |
May be present |
NP/I |
The cache block at this chip is invalid. |
Not in L2, but in L1 or more L1s(2) |
May be present at other chips |
ILS |
The cache block is not present at L2 on this chip. It is shared locally by L1 nodes in this chip. |
ILO |
The cache block is not present at L2 on this chip. Some L1 node in this chip is an owner of this cache block. |
||
ILOS |
The cache block is not present at L2 on this chip. Some L1 node in this chip is an owner of this cache block. There are also L1 sharers of this cache block in this chip. |
||
Not present at any other chip |
ILX |
The cache block is not present at L2 on this chip. It is held in exclusive mode by some L1 node in this chip. |
|
ILOX |
The cache block is not present at L2 on this chip. It is held exclusively by this chip and some L1 node in this chip is an owner of the block. |
||
ILOSX |
The cache block is not present at L2 on this chip. It is held exclusively by this chip. Some L1 node in this chip is an owner of the block. There are also L1 sharers of this cache block in this chip. |
||
In L2, but not in any L1(3) |
May be present |
S |
The cache block is not present at L1 on this chip. It is held in shared mode at L2 on this chip and is also potentially shared across chips. |
O |
The cache block is not present at L1 on this chip. It is held in owned mode at L2 on this chip. It is also potentially shared across chips. |
||
Not present |
M |
The cache block is not present at L1 on this chip. It is present at L2 on this chip and is potentially modified. |
|
Both in L2, and 1 or more L1s(4) |
May be present |
SLS |
The cache block is present at L2 in shared mode on this chip. There exist local L1 sharers of the block on this chip. It is also potentially shared across chips. |
OLS |
The cache block is present at L2 in owned mode on this chip. There exist local L1 sharers of the block on this chip. It is also potentially shared across chips. |
||
Not present |
OLSX |
The cache block is present at L2 in owned mode on this chip. There exist local L1 sharers of the block on this chip. It is held exclusively by this chip. |
这些Stable状态位分为两大部分,4个小组,一部分用于反映L2 Cache的Coherence状态,另一部分用于CMP内部的多个L1 Cache。
- 第1组是{NP, I},表示在本CMP中Cache Block无效,可能其他CMP具有数据副本。
- 第2组是{ILX, ILOX, ILOSX, ILS, ILO, ILOS}。其中{ILX, ILOX, ILOSX}确定当前CMP是否对一个Cache Block具有排他的Ownership,即Exclusive Ownership,其他CMP不会有数据副本,当然这并不保证当前CMP某个CPU Core也具备这种Exclusive Ownership;{ ILS, ILO, ILOS},保证在当前CMP中的L2 Cache没有数据副本,但是并不保证其他CMP具有数据副本。
- 第3组为{S, O, M},这些状态是对于L2 Cache而言,传统的S, O和M状态位,此时当前CMP中的L1 Cache不含有数据副本。
- 第4组为{ SLS, OLS, OLSX}与第3组类似,只是在当前CMP的L1 Cache中存在副本。
第1, 3和4组状态位组成的FSM,即Intra-Chip Inclusion FSM如图4‑11所示。
{ILX, ILOX, ILOSX, ILS, ILO, ILOS}这些状态位属于L2 Cache Block,却与L1 Cache有直接的联系。由这些状态组成的FSM如图4‑12所示。
在与L1 Cache Block相关的状态位中,{ILX, ILOX, ILOSX}状态位可以反映当前CMP是否Exclusive Owned Cache Block,此时进行状态迁移时,不用向CMP Directory发送GETX请求。当然Cache Block处于M,OLSX时,也不需要向CMP Directory发送GETX请求;否则都需要发送GETX请求。如何实现这个GETX请求与CMP处理器间使用的网络拓扑结构相关,可以是广播,也可以是指定的CMP。
无论采用哪一种种CMP间的一致性,在Directory Controller中所使用的状态位都不及L1或者L2 Cache Controller复杂。在CMP_directory Protocol中的Directory Controller设置了4个状态位,如下所示。
- M位有效时表示Cache Block仅在当前CMP中具有有效数据副本,可能与主存储器不一致,也可能一致,即包含传统的E。
- O位有效时表示Cache Block在当前CMP中具有有效数据副本,而且在其他CMP中含有数据副本。数据副本与主存储器不一致。
- S位有效时表示Cache Block在当前CMP中具有有效数据副本,而且在其他CMP中含有数据副本。数据副本与主存储器可能一直,也可能不一致。
- I位有效表示Cache Block无效。
由这些状态位组成的FSM可能是最简单的,如图4‑13所示。
我一直盼望整个Cache Controller的FSM只有图4‑10,图4‑11,图4‑12和图4‑13所示的内容这样简单。倘若真的如此,我们即便再多引入一级Cache结构也并不会太复杂,依然存在单个个体就能够理解整个Cache Hierarchy的可能。
4.8 Beyond MOESIF 2
事实上在图4‑10中,O不能直接迁移到I;在图4‑11中O不能直接迁移到I;在图4‑12中,ILO不能直接迁移到ILX;在图4‑13中,O也不能直接迁移到I。在整个Cache Hierarchy的设计中,合理有效解决因为Memory Consistency引发的Race Condition贯彻始终,也引入了过多的所谓“Safe State”,这些Safe State被称为Transient State。我们以图4‑10中简单到不能再简单的I到MM_W的状态迁移说明这些Transient State的作用。
L1 Cache与Sequencer直接相连,当CPU Core进行一次存储器Store操作,将引发一系列复杂的操作,这些操作不仅与采用的Cache Coherence Protocol相关,与采用的Write Policy和使用的Memory Consistency模型也有直接关系。为简化起见,我们选用Weekly Ordered Memory Model,Write Policy为Write-Back Write-Allocate,Coherence Protocol为本节所重点描述的MOESI_CMP_directory。
即便在这种情况之下,我依然会省去更多的细节,忽略绝大多数状态信息和绝大多数总线Transaction,仅书写Store操作中的一种状态转移情况。我希望可以在一个较少的篇幅内完成最基本的说明,给予对这部分内容有兴趣的读者一个入门级别的描述。
在CPU Core中,一个Store操作,引起的结果异常深远,这个Store操作首先到达L1 Cache Controller,之后从一个Stable State进入到一个Safe State,之后穿越当前CMP处理器系统的L2/Directory Cache Controller,到达和这个Store操作相关的其他CMP处理器系统后,再逐级回溯到当前CMP处理器的L1 Cache Controller后,再次进入到另一个Safe State,最终小心翼翼地完成整个操作后进入到最终的Stable State。对于MOESI_CMP_directory,I状态转移到MM_W状态,需要经历IM,OM两个Transient State状态,其过程如图4‑14所示。
本次Store操作很幸运,因为在其Miss时,恰好有一个状态位为I的未用Cache Block,这种情况甚至比在Cache中Hit容易处理。首先I状态将迁移到IM状态,在迁移的过程中,完成Cache Block的分配,发送GETX操作,获得当前Cache Block的控制权。
在IM状态时,如果收到Exclusive_Data或者Data后,将迁移到OM状态,在迁移的过程中,将进行数据Merge并写入Cache Block中。为了维护Store的Global View的统一,此时虽然数据已经写入到Cache Block,依然不能通知CPU Core当前Store操作完成,必须要等待CMP处理器系统中所有CPU Core的ACK。一次GETX操作可能会产生多个ACK。当收齐所有ACK后,L1 Cache Controller将Trigger All_acks这个重要的Message。
GEM5 Simulator依然简化了IM状态的处理,因为Data/Exclusive_Data和ACK可能是异步的,即便收到了所有的ACK,Data/Exclusive_Data也未必到达,因为ACK与Data的传递可能使用了不同的路径。更为重要的是什么叫All_acks,不同的Protocol采用的方法并不相同。
Intel的MESIF Protocol[96]引入了一个F(Forward)状态。在ccNUMA处理器系统中,可能在多个处理器的Cache中存在相同的数据副本,在这些数据副本中,只有一个Cache Block的状态为F,其他Cache Block的状态为S。
当一个数据请求方读取这个数据副本时,只有状态为F的Cache行,可以将数据副本转发给数据请求方,状态位为S的Cache不能转发数据副本。数据请求方从状态位为F的Cache Block中收到ACK后,即认为收齐了所有ACK。这种方法可以减少Bus Traffic,其他ccNUMA处理器会采用其他方法避免这种Bus Traffic,谈不上是伟大的发明。
在OM状态收到All_acks后,最终迁移到MM_W状态,此时首先通知CPU Core Store操作执行完毕,然后宣布当前Cache Block处于Exclusive状态,之后设置Timeout,在经过一段延时之后,MM_W状态将自动迁移到MM状态。
在MOESI_CMP_directory Protocol的L1 Cache Controller中与IM和OM类似的Transient State还有SM,IS,SI,OI,MI和II,共计8个;L2 Cache Controller中有50个这样的兄弟;Directory Controller中有15个这样的姐妹。所有这些状态累积在一起形成的FSM不应该用2维方式进行描述,至少需要3维。
我曾试图用2维FSM描述MESI_CMP_directory Protocol,最后发现无法找到尺寸合适的纸张。也许使用PDA(Pushdown Automation)是一个不错的想法,有时间我会进行这方面的尝试。更有人明知我看不懂和Quantum相关的任何内容,依然愚弄我,向我推荐了QFA(Quantum Finite Automata)。
这些SLICC描述可以方便地转换为HTML文件,通过点击鼠标就可发现State之间的迁移,如http://www.cis.upenn.edu/~arraghav/protocols/VI_directory/L1Cache.html所示,只是在状态过多时,使用这样的方法并不方便。
过多的状态增加了Cache Coherence Protocol的设计复杂度,即便是GEM5的实现也并不容易完全掌握。每次看到莘辛学子们强读GEM5,有种在刀尖上行走的感觉,为他们骄傲的同时祝他们好运。GEM5不是Cache Hierarchy的全部,更不是Cache Coherence Protocol的全部。GEM没有实现最基础的atomic操作,没有实现Memory Barrier,没有连接Cache间的Buffer,也没有更多的Cache层次结构,如L3 Cache,没有太多的实现细节。
GEM5并没有提供有效的Verification策略。而在某种意义上说,Cache Verification的难度甚至超过Design。彻底验证哪怕是最简单的Cache Coherence Protocol也并不是如想象中简单,不仅在于Verification的过程中,会遇到牛毛般匪夷所思的几角旮旯,还在于Verification策略本身的完备。
Design和Verification间存在着密切联系,存在着诸多选择。这使得在一个实际的Cache Hierarchy设计中,取舍愈发艰难。在你面前有太多的选择,无法证明哪一个更为有效,在你面前也没有什么Formal Proof Methods。
在人类的智慧尚没有解决交换舞伴这样平凡的NP Hard的前提下,可能最完美最有效的策略只有O(N!)级别的穷举。这些平凡的问题一直等待着不平凡的解读。解决这些问题的人的不朽将远远超越计算机科学这个并不古老的学科。这一切羞辱着天下人的智慧,令世界上最快的K Computer不堪重负。更多的人去依赖没有那么可靠的Quantitative Approach。在这些Approach中,谁去验证了所使用的Models,Theories和hypotheses。
计算机科学在这些不完美中云中漫步。几乎没有人知道准确的方向。有些技巧是没有人传授,因为没有人曾经尝试过。这使得这个星球最顶级的Elite,甘愿穷其一生去做猎物,等待猎人的枪声。没有人欣赏这种方法。更多的是饥饿着的愚钝着的执着。
Stay Hungry,Stay Foolish。
4.9 Cache Write Policy
如果有可能,我愿意放弃使用Write操作,Write操作不是在进行单纯地进行写,而是将Read建立的千辛万苦异常小心地毁于一旦,是Cache Hierarchy设计苦难的发源地。没有写操作在Cache Block中就不会有这么多状态,更没有复杂的Memory Consistency模型。
很多人都不喜欢写操作,这并不能阻挡它的真实存在。绝大多数人痛恨写操作,也不愿意去研究如何提高写操作的效率,写虽然非常讨厌,但是很少在速度上拖累大家,只是写太快了,带来了许多副作用经常影响大家。不允许写操作使用Cache貌似是个方法,却几乎没有程序员能够做到这一点。过分的限制写操作也并非良策,凡事总有适度。
这使得在Cache Hierarchy设计中读写操作得到了区别对待。读的资质差了一些,有很多问题亟待解决,最Critical的是如何提高Load-Use Latency;对于写,只要不给大家添乱就已经足够,Cache Bus的Bandwidth能少用点就少用点,能降低一点Bus Traffic就算一点,实在不能少用,就趁着别人不用的时候再用。从总线带宽的角度上看,Load比Store重要一些,在进行Cache优化时,更多的人关心读的效率。
在一个程序的执行过程中不可能不使用Write操作。如何在保证Memory Consistency的前提下,有效降低Write操作对Performance的影响,如何减少Write Traffic,是设计的重中之重。在一个处理器系统中,Write操作需要分两种情况分别讨论,一个是Write Hit,另一个是Write Miss。Write Hit指在一次Write操作在进行Probe的过程中在当前CMP的Cache Hierarchy中命中,而Write Miss指没有命中。
我们首先讨论Write Hit,从直觉上看Write Hit相对较为容易,与此相关的有些概念几乎是常识。Write Hit时常用的处理方法有两种,一个是Write Through,另一个是Write Back[1]。相信绝大部分读者都明白这两种方法。Write Through方法指进行写操作时,数据同时进入当前Cache,和其下的一级Cache或者是主存储器;Write Back方法指进行写操作时,数据将直接写入当前Cache,而不会继续传递,当发生Cache Block Replace时,被改写的数据才会更新到其下的Cache或者主存储器中。
很多人认为Write-Back在降低Write Traffic上优于Write-Though策略,只是Write-Back的实现难于Write-Though,所以有些低端处理器使用了Write-Though策略,多数高端处理器采用Write-Back策略。
这种说法可能并不完全正确,也必将引发无尽的讨论。在没有拿到一个微架构的全部设计,在没有对这个微架构进行系统研究与分析之前,并不能得出其应该选用Write-Through或者是Write-Back策略的结论。即便拿到了所有资料,进行了较为系统的Qualitative Research和Quantitative Analysis之后,你也很难得出有说服力的结论。
在目前已知的高端处理器中,SUN的Niagara和Niagara 2微架构的L1 Cache使用Write-Through策略[81],AMD最新的Bulldozer微架构也在L1 Cache中使用Write-Through策略[74]。Intel的Nehalem和Sandy Bridge微架构使用Write-Back策略。Write-Through和Write-Back策略各有其优点和不足,孰优孰劣很难说清。
Norman P. Jouppi列举了Write-Through策略的3大优点,一是有利于提高CPU Core至L1 Cache的总线带宽;二是Store操作可以集成在指令流水线中;三是Error-Tolerance [97]。这篇文章发表与1993年,但是绝大部分知识依然是Cache Write Policies的基础,值得借鉴。
其中前两条优点针对Direct Mapped方式,非本节的重点,读者可参考[97]获得细节。制约Cache容量进一步增大的原因除了成本之外,还有Hit Time。如上文已经讨论过的,随着Cache容量的增加,Hit Time也随之增大。而Cache很难做到相对较大,这使得选择Direct Mapped方式时需要挑战John和David的2:1 Cache Rule of Thumb,目前在多处处理器系统中都不在使用Direct Mapped方式。
对这些Rule of Thumb的挑战都非一蹴而就。在某方面技术取得变革时,首先要Challenge的恰是之前的Rule of Thumb。善战者择时而动,而善战者只能攻城掠地,建立一个与旧世界类同的新世界,不会带来真正的改变。
革新者远在星辰之外,经得住大悲大喜,大耻大辱,忍受得住Stay Hungry Stay Foolish所带来的孤单寂寞。是几千年前孟子说过的“富贵不能淫,贫贱不能移,威武不能屈。居天下之广居,立天下之正位,行天下之大道。得志,与民由之,不得志,独行其道”。几千年祖辈的箴言真正习得是西方世界。
忽略以上这段文字。我们继续讨论Norman P. Jouppi提及的使用Write-Through策略的第三个优点,Error-Tolerance。CMOS工艺在不断接近着物理极限,进一步缩短了门级延时,也增加了晶体管的集成度,使得原本偶尔出现的Hard Failures和Soft Errors变得更加濒繁。这一切影响了SRAM Cells的稳定性,导致在基于SRAM Cells的Cache Memory中,Hard和Soft Defects不容忽视。
采用Write-Back策略的Cache很难继续忍受Single Bit的Defects,被迫加入复杂的ECC校验;使用Write-Though策略,Cache因为仍有数据副本的存在只需加入Parity Bit。与ECC相比,Parity Bit所带来的Overhead较小。这不是采用ECC校验的全部问题,在一个设计中,为了减少Overhead,需要至少每32位或者更多位的参与产生一次ECC结果。这些Overhead的减少不利于实现Byte,Word的Store操作,因为在进行这些操作时,需要首先需要读取32位数据和ECC校验,之后再Merge and Write新的数据并写入新的ECC校验值。
除此之外使用Write-Though策略可以极大降低Cache Coherence的实现难度,没有Dirty位使得多数设计更为流畅。但是你很难设想在一个ccNUMA处理器系统中,Cache的所有层次结构都使用Write-Though策略。由此带来的各类Bus Traffic将何等壮观。
采用Write-Though策略最大的缺点是给其他Cache层次带来的Write Traffic,而这恰恰是在一个Cache Hierarchy设计过程中,要努力避免的。为了降低这些Write Traffic,几乎所有采用Write-Though策略的高端处理器都使用了WCC(Write Coalescing Cache)或者其他类型的Store-Though Queue,本节仅关注WCC。
假设一个微架构的L1 Cache采用Write-Through方式。WCC的作用是缓冲或者Coalescing来自L1 Cache的Store操作,以减少对L2 Cache的Write Traffic。在使用WCC时,来自L1 Cache的Store操作将首先检查WCC中是否含有与其地址相同的Entry,如果有则将数据与此Entry中的数据进行Coalescing;如果没有Store结果将存入WCC的空闲Entry中;如果WCC中没有空闲Entry则进行Write Through操作。WCC的大小决定了Write Traffic减少的程度。WCC的引入也带来了一系列Memory Consistency问题。
采用Write-Back策略增加了Cache Coherency设计难度,也有效降低了Write Traffic,避免了一系列Write-Through所带来的问题。下文以GEM5中的MOESI_CMP_directory为例简要Write-Back策略的实现方法。
如果Write Hit的Cache Block处于Exclusive[2]状态时,数据可以直接写入,并通知CPU Core Write操作完成。Write Hit的Cache Block处于Shared或者Owner状态时的处理较为复杂。即便是采用Write-Through策略,这种情况的处理依然较为复杂,只是Cache间的状态转换依然简单很多。本节仅介绍命中了Shared状态这种情况。
在L1 Cache层面,这个Write Hit命中的Cache Block,其状态将会从S状态迁移到MM_W状态,之后经过一段延时迁移到MM状态。在MOESI_CMP_directory Protocol中,如果L2 Cache Block的状态为ILS,ILO,ILOS,SLS,OLS和OLSX时,L1 Cache中必定含有对应的状态为S的Block。由于Accidentally Inclusive的原因,L1 Cache Block多数时候在L2 Cache中具有副本,因此必须要考虑L1 Cache Block进行状态迁移时对L2 Cache Controller的影响。不仅如此还需要考虑CMP间Cache Coherency使用的Directory。
我们首先考虑L1 Cache Block的从S开始的状态迁移过程。当CPU Core的Cache Hit到某个状态位S的L1 Cache Block后便开始了一次长途旅行。在这个Clock Block的S状态将经由SM,OM,最终到达MM_W和MM状态,如图4‑15所示。
CPU Core进行Store操作,Cache Hit一个状态为S的Block时,首先迁移到SM状态,之后在收到Data或者Exclusive Data之后进入到OM状态,OM状态到MM_W状态的迁移过程与图4‑14的过程类似。本节重点关注b_issueGETX请求,这个请求被称为Read for Exclusive。
在一个ccNUMA处理器系统中,b_issueGETX请求虽然复杂依然有机可循,首先在Intra-CMP中尝试并获得对访问Cache Block的Exclusive权限。如果没有获得Exclusive权限,则将这个请求转发给Directory Controller,并由Home Agent/Node经由CMP间的互联网络发送给其他CMP直到获得Exclusive权限。
在MOESI_CMP_directory Protocol中,Cache Block的状态为S表示在当前ccNUMA处理器系统中至少还存在一个数据副本,因此b_issueGETX请求将首先在Intra-CMP中进行,并通过requestIntraChipL1Network_out发送至L2 Cache Controller。
L2 Cache Controller通过L1requestNetwork_in获得b_issueGETX请求,如果在L2 Cache中包含访问的数据副本,b_issueGETX请求将转换为L1_GETX请求。在L2 Cache中包含L1 Cache Block为S状态数据副本的情况有很多,还有一些L2 Cache Block包含数据副本,但是在当前CMP中其他L1 Cache中也包含数据副本的情况。这些状态转移的复杂程度如果我能够用语言简单描述,就不会有SLICC这种专用语言的存在价值。
如果在Intra-CMP中可以处理b_issueGETX请求,并不算太复杂,如果不能,而且L2 Cache Block的状态为SLS时,需要进一步做Inter-CMP Coherence的处理,此时b_issueGETX请求将转换为a_issueGETX,并通过globalRequestNetwork_out继续发送至Directory Controller。此时Directory Controller通过requestQueue_in获得该请求,并将其转换为GETX请求。Directory中具有4个Stable状态M,O,S和I,不同的状态对于GETX请求的处理不尽相同。
如果处于O状态,此时Intra-CMP为该Cache Block的Owner,此时仅需要向其他Inter-CMP发送g_sendInvalidations请求,并可将Exclusive_Data转发至上层,L1 Cache Block的状态也因此迁移为OM。
如果在Directory中没有记录哪些CMP中具有状态为S的数据拷贝时,g_sendInvalidations请求将广播到所有参与Cache Coherence的CMP处理器中,这种方式带来的Bus Traffic非常严重,也是这个原因在Directory中多设置了Bit Vectors,用来记录哪些CMP中具有状态为S的数据副本。
在这种情况下g_sendInvalidations请求仅需发送到指定的CMP,而不需要进行广播。因为Memory Consistence的原因,发起请求的CMP必须要等待这些指定的CMP返回所有的ACK后,才能进一步完成CPU Core的Store操作。除了在Directory中设置了Bit Vectors之外,Intel的MESIF中的F状态也可以在某种程度上避免因为过多的ACK带来的Bus Traffic。
通常情况在CMP间的连接拓扑不会使用Share Bus方式,对于Request/ACK这种数据请求模式,采用Share Bus方式最大的好处是具有一个天然的全局同步点,这个同步点在严重影响了总线带宽的前提下,极大了降低了设计难度。
采用Share Bus方式时,随着总线上节点数目的增加,冲突的概率也以O(N!)级别的算法复杂度进一步增长。这使得一个可用的CMP间的互联方式极少采用Share-Bus方式。采用其他方式,无论是Ring-Bus或者更加复杂的拓扑结构,都会涉及到多个数据通路,这使得从这些CMP返回的ACK并没有什么顺序,Home Agent/Node不能接收到一个ACK就向上转发一次,而是收集后统一转发。
当L1 Cache Block收到All_acks后,将从OM状态转移为MM_W状态,完成最后的操作,其过程与图4‑14所示相同。为简略起见,本节不再介绍在Directory中是M,S和I状态的情况。而专注与Write Miss的处理。
Norman P. Jouppi从两个方面讨论Write Miss的处理策略。首先是否为每一个Miss的请求重新准备一个Cache Block,因此产生了两种方法,Write-Allocate或者No-Write Allocate。如果使用Write-Allocate方法,Cache Controller为Miss请求在当前Cache中分配一个新的Cache Block,否则不进行分配;
其次是否需要从底层Cache中获取数据,因此产生了两种处理方法,Fetch-on-Write或者No-Fetch-on-Write方法,如果采用Fetch-on-Write方法,Cache Controller将从其下Cache层次结构中Fetch已经写入的数据,并进行Merge操作后统一的进行写操作,否则不进行Fetch。还有一种方法是Write-Before-Hit,这种实现方法多出现在Direct Mapped Write Through Cache中,本节对此不再关注。
Write-Allocate与Fetch-on-Write方法没有必然联系,但是这两种方法经常被混淆,以至于后来没有更正的必要与余地,通常意义上微架构提到的Write-Allocate策略是Write-Allocate与Fetch-on-Write的组合;实际上Write-Allocate也可以与No-Fetch-on-Write混合使用,该方法也被称为Write-Validate,这种方法很少使用;No-Write Allocate与No-Fetch-on-Write的组合被称为Write-Around,如表4-5所示。
表4-5 Write-Allocate与Fetch-on-Write的组合[3]
|
Fetch-on-Write |
No-Fetch-on-Write |
Write-Allocate |
Write-Allocate |
Write-Validate |
No-Write Allocate |
N.A. |
Write-Around |
我们假设一个处理器系统的Memory Hierarchy包含L1,L2 Cache和主存储器,而Cache Miss发生在L1 Cache。并在这种场景下,分析Write-Allocate,Write-Validate和Write-Around这三种方法的实现。
当Write L1 Cache Miss而且使用Write-Allocate方法时,L1 Cache Controller将分配一个新的Cache Block,之后与Fetch的数据进行合并,然后写入到L1 Cache Block中。这种方法较为通用,但是带来了比较大的Bus Traffic。
新分配一个Cache Block往往意味着Replace一个旧的Cache Block,如果这个Cache Block中含有Dirty数据,Cache Controller并不能将其Silent Eviction,而是需要进行Write-Back;其次Fetch操作本身也会带来不小的Bus Traffic。
Write-Around策略是No-Write Allocate和No-Fetch-on-Write的策略组合。使用这种方法时,数据将写入到L2 Cache或者主存储器,并不会Touch L1 Cache。当Cache Miss时,这种方法并不会影响Memory Consistency。但是这种方法在Cache Hit时,通常也会Around到下一级缓冲,这将对Memory Consistency带来深远的影响。
虽然我能构造出很多策略解决这些问题,但是这些方法都不容易实现。读者可以很自然的想到一种方法,就是在Cache Hit时不进行这个Around操作,对此有兴趣的读者可以进一步构想在这种情况之下,如何在保证Memory Consistency的情况较为完善的处理Cache Hit和Miss两种情况。本节对此不再进一步说明。
Write-Validate策略是No-Fetch-on-Write和Write-Allocate的策略组合。使用这种方法时,L1 Cache Controller首先将将分配一个新的Cache Block,但是并不会向Fetch其下的Memory Hierarchy中的数据。来自CPU Core的数据将直接写入新分配的L1 Cache Block中,使用这种方法带来的Bus Traffic非常小。
但是来自CPU Core的数据不会是Cache Block对界操作,可能是Byte,Word或者是DWord,L1 Cache必须要根据访问粒度设置使能位,这个使能位可以是By Byte,Word或者是Dword,也因此带来的较大的Overhead。当进行Cache Write操作时,除了要写入的数据的使能位置为有效外,其他所有位都将置为无效。在使用这种方法时,有效数据可能分别存在与L1 Cache和L2 Cache,这为Memory Consistence的实现带来了不小的困难。
No-Write Allocate和Fetch-on-Write的策略组合没有实际用途。从其下Memory Hierarchy Fetch而来的数据因为没有存放位置,在与CPU Core的数据进行合并后,依然需要发送到其下的Memory Hierarchy。几乎没有什么设计会进行这种不必要的两次总线操作。
在Write Miss的处理方法中,Write-Allocate和Write-Around策略较为常用,Write-Validate策略并不常用。Write Miss的处理方法与Write Hit存在一定的依赖关系。Write-Around需要与Write-Through策略混合使用,而Write-Allocate适用于Write-Through和Write-Back策略。
在一个ccNUMA处理器系统中,与Write操作相关的处理更为复杂。本节介绍的Hit Miss实现策略各有其优点,所有这些策略所重点考虑的依然是如何降低因为Write而带来的Bus Traffic和Memory Consistency。
[1] 还有一种是上文提及的Write Once,Write Once是Write Through和Write Back的联合实现。
[2] M,M_W,MM和MM_W状态都属于Exclusive状态。
[3] 表4-5源自[74],并有所改动。
4.10 Case Study on Sandy Bridge Cache Load
这一节是我准备最后书写的内容,在此之前最后一章的书写早已完成。待到结束,总在回想动笔时的艰辛。这些艰辛使我选择一个Case Study作为结尾,因为这样做最为容易。这些Case实际存在的,不以你的喜好而改变。缺点与优点都在你面前,你无需改变,只需要简单的去按照事实去陈述。
Sandy Bridge是Nehalem微架构之后的Tock,并在Nehalem的基础上作出的较大的改动。本节重点关注Cache Hierarchy上的改动。鉴于篇幅,鉴于没有太多公开资料,我并不能在这里展现Sandy Bridge微架构的全貌,即便只限于Cache Hierarchy层面。这一遗憾给予我最大的帮助是可以迅速完成本节。
Intel并没有公开Sandy Bridge的细节,没有太多可供检索的参考资料。David Kanter在Realworldtech上发表的文章[69][99]较为详尽,但这并不是来自官方,鉴于没有太多公开资料,本篇仍然使用了David Kanter的文章。虽然我知道真正可作为检索的资料是Intel发布的Intel 64 and IA-32 Architectures Optimization Reference Manual中的第2.1.1节[98]和Intel在2010年IDF上公开的视频http://www.intel.com/idf/audio_sessions.htm[1]。
Sandy Bridge包含两层含义。首先是Sandy Bridge微架构,即Core部分,由指令流水,L1 Cache和L2 Cache组成,如图4‑16所示。其中指令流水的Scheduler部件,和Load,Store部件需要重点关注,最值得关注的是L1和L2 Cache的组成结构。
从CPU Core的角度上看,Sandy Bridge与Nehalem相比,并没有太多质的变化。最值得关注的是Sandy Bridge增加的L0 Instruction Cache和PRF(Physical Register File)。L0 Instruction Cache也被称为Decoded μops Cache,这是Sandy Bridge在指令流水中相对于Nehalem微架构的重大改进。PRF替换了Nehalem微架构使用的CRRF(Centralized Retirement Register File)。PRF不是什么新技术,只是Intel实现的晚了些。
在Core和Nehalem微架构中,每一个μops包含Opcode和Operand。这些μops在经过指令流水执行时需要经过若干Buffer,有些Buffer虽然只需要Opcode,但是也必须要同时容纳Operand,因而带来了不必要的硬件开销。在Core微架构时代,Operand最大为80b,Nehalem为128b,到了Sandy Bridge微架构,Operand最大为256b。
如果Sandy Bridge不使用PRF,支持AVX(Advanced Vector Extension)的代价会变得无法承受,因为有些AVX指令的Operand过长。AVX的出现不仅影响了指令流水线的设计,也同时影响了Sandy Bridge的Memory子系统的设计。我们首先关注指令执行部件中的Memory Cluster,Memory Cluster即为LSU,其结构如图4‑17所示。
与Nehalem微架构在一个Cycle中只能执行一条128b的Load和Store指令[1][12]相比, Sandy Bridge微架构在一个Cycle中可以执行一条128b的Load和一条128b的Store指令,或者两条Load指令,进一步提高了Load指令的执行效率,在微架构设计中,通常会优先考虑Load指令,而不是Store指令。如果将Store指令提高为两条,其中因为Memory Consistency引发的同步并不容易处理。也因为这个原因,Sandy Bridge设置了两条Load通路,LSU与L1 Data Cache间的总线宽度也从Nehalem微架构的2×128b提高到3×128b。
合并Load和Store Address部件在情理之中,因为Load操作和Store Probe操作有相近之处,在现代处理器中,Store操作的第一步通常是Read for Ownership/Exclusive,首先需要读取数据后,再做进一步的处理。
在Sandy Bridge中,FLC和MLC的组成结构与Nehalem微架构类同。最大的改动显而易见,是在L1 Cache之上多加了一个读端口。单凭这一句话就够工程师忙碌很长时间。在Cache Memory层面任何一个小的改动,对于工程师都是一场噩梦。
其中FLC由指令Cache与数据Cache组成,由两个Thread共享;MLC为微架构内部的私有Cache。L1指令和数据Cache的大小均为32KB,MLC的大小为256KB。FLC和MLC的关系为NI/NE,组成结构为8-Way Set-Associative,Cache Block为64B,MPMB,Non-Blocking,Write-Allocate,Write Back和Write-Invalidate。Cache Coherence Protocol为MESI。
这些仅是Sandy Bridge微架构,即Core层面的内容。Sandy Bridge的另一层含义是Sandy Bridge处理器。Sandy Bridge处理器以Sandy Bridge微架构为基础,包括用于笔记本和台式机的Sandy Bridge处理器,和Server使用的Sandy Bridge EP处理器。但是Sandy Bridge和Sandy Bridge EP在Uncore部分的设计略有不同,本节重点讲述Sandy Bridge EP处理器。
Sandy Bridge EP处理器由CPU Core,iMC控制器(Home Agent)[98],Cache Box[1][99],PCIe Agent,QPI Agent和LLC(L3 Cache)组成,由Ring Bus(Ring-Based的Interconnect)连接在一起,并在其内部集成Graphics Controller,其组成结构如图4‑18所示。
其中Cache Box是Core与Uncore部分的连接纽带。如图4‑18所示,Cache Box提供了三个接口,与CPU Core,LLC和Ring Bus的接口。Cache Box的主要功能是维护Sandy Bridge EP处理器中CPU Core与Core间的Memory Consistency,并将来自CPU Core的数据请求发送到合适的LLC Slice或者其他设备中[1]。
Sandy Bridge EP处理器的LLC采用Distributed方式,每一个CPU Core都有一个对应的LLC Slice,每个Slice的大小可以是0.5/1/1.5/2MB,可以使用4/8/12/16-Way Associated方式。这不意味着每一个CPU Core都有一个私有Slice。
来自CPU Core的数据访问在经过Cache Box时,首先进行Hush,并通过Ring Bus转发到合适的Cache Slice。但是从逻辑层面上看这些Slice组成一个LLC。Sandy Bridge EP处理器的LLC与Core内Cache的关系是Inclusive,与Nehalem的L3 Cache类同。这意味着空间的浪费,也意味着天然的Snoop Filter[99]。
所有CPU Core,LLC Slice,QPI Agent,iMC,PCIe Agent,GT(Graphics uniT)通过Ring Bus连接在一起[1][99]。Ring Bus是Sandy Bridge EP处理器的设计核心,也意味着GT可以方便的与CPU Core进行Cache Coherence操作。这在一定程度上决定了Sandy Bridge处理器横空出世后,基于PCIe总线的Nvidia Graphics Unit黯然离场。
Sandy Bridge EP处理器的Ring Bus,采用Fully Pipelined方式实现,其工作频率与CPU Core相同,并由四个Sub-Ring Bus组成,分别是Data Ring,Request Ring,Acknowledge Ring和Snoop Ring[1],其中Data Ring的数据宽度为256位。这些Sub-Ring Bus协调工作,共同完成Ring Bus上的各类总线Transaction,如Request,Data,Snoop和Response。采用4个Sub-Ring Bus可以在最大程度上使不同种类的Transaction并发执行。
Sandy Bridge EP处理器的这些Sub-Ring Bus谈不上是什么创新,所有使用了Ring Bus结构的现代处理器都需要这么做。由于Dual Ring的存在,Sub-Ring中通常含有两条总线,可能只有Snoop Ring除外,所以在Sandy Bridge的Ring Bus至少由7条Bus组成[1]。
在Ring Bus上,还有两个重要的Agent,一个是Memory Agent,另一个是QPI Agent。其中Memory Agent用来管理主存储器,包括iMC,而QPI Agent用于管理QPI链路,并进行与其他Sandy Bridge EP处理器互联,组成较为复杂的ccNUMA处理器系统。
以上是对Sandy Bridge EP处理器与Memory Hierarchy结构的简单介绍,下文将以此为基础进一步说明Sandy Bridge EP处理器如何进行Load操作。
剩余的内容需要等待Intel公开Sandy Bridge EP使用的Transaction Flow,估计会在Sandy Bridge EP正式发布时公开。Sandy Bridge EP的正式发布推迟到了2012年Q1,那时我会重新书写本节。整篇文章需要更改的地方还有很多。
[1] 这些说法仅是猜测。Snoop Ring有两条总线,至少我现在想不出什么简单的方法确保Memory Consistency。