Data Prefetch
处理器与存储器子系统运行速度的失配,使得存储器层次结构多次引起关注,处理器系统使用了更大规模的Cache。在很多处理器系统中,LLC的大小已达十几兆字节。随着工艺的提高,使用更大规模的Cache容量,并非遥不可及。只是Cache容量依然远不能与主存储器容量增加的速度相比。在某些应用中,即便将现有的Cache容量提高一倍也于事无补。
存储器访问在最后一级Cache中Miss后,指令流水可能会被迫Stall,有些执行部件甚至要为此等待几百个Cycle,极大降低了处理器的整体运行效率。在这种情况之下,使用再精巧的指令流水线设计也无能为力。
这一切使得更多的人重新考虑存储器子系统的延时处理。各种想法层出不穷,如更加充分利用Non-Blocking Cache流水线,容纳上千条指令的OOO指令流水,Runahead执行,Prefetch等等。这些想法并非天方夜谭,具有理论基础与量化数据作为支撑。这些想法不是绝对的真理,可能只是Trade-Off。在这些想法中,目前使用最多的,最为成功的是Prefetch。
5.1 数据预读
Prefetch指在处理器进行运算时,提前通知存储器子系统将运算所需要的数据准备好,当处理器需要这些数据时,可以直接从这些预读缓冲中,通常指Cache,获得这些数据,不必再次读取存储器,从而实现了存储器访问与运算并行,隐藏了存储器的访问延时。Prefetch的实现可以采用两种方式,HB(Hardware-Based)和SD(Software-Directed)。这两种方法各有利弊,我们首先以图5‑1为基础模型讨论采用SD方式的数据预读。
![5.1 <wbr>数据预读 5.1 <wbr>数据预读](https://i-blog.csdnimg.cn/blog_migrate/cfce5da538fc9c5384d0f70a3deef8b6.jpeg)
其中实例a没有使用预读机制;实例b是一个采用预读机制的理想情况;实例c是一个采用预读机制的次理想情况。我们假设处理器执行图5‑1所示的任务需要经历四个阶段,每个阶段都由处理器执行运算指令和存储指令组成。
在其中处理器的一次存储器访问需要5个时钟周期。在第一个阶段处理器执行4个时钟周期后需要访问存储器;在第二个阶段处理器执行6个时钟周期后需要访问存储器;在第三个阶段处理器执行8个时钟周期后需要访问存储器;在第四个阶段处理器执行4个时钟周期后完成整个任务。
实例a没有使用预读机制,在运算过程中,在进行存储器访问,将不可避免的出现Cache Miss。执行上述任务共需40个时钟周期。使用预读机制可以有效缩短整个执行过程。在实例b中在执行过程中,会提前进行预读操作,虽然这些预读操作也会占用一个时钟周期,但是这些预读操作是值得的。合理使用这些数据预读,完成同样的任务CPU仅需要28个时钟周期,从而极大提高了程序的执行效率。
这种情况是非常理想的,处理器在执行整个任务时,从始至终是连贯的,处理器执行和存储器访问完全并行,然而这种理想情况并不多见。在一个任务的执行过程中,并不容易确定最佳的预读时机;其次采用预读所获得数据并不一定能够被及时利用,因为在程序执行过程中可能会出现各种各样的分支选择,有时预读的数据并没有被及时使用。
在实例c中,预读机制没有完全发挥作用,所以处理器在执行任务时,Cache Miss仍会发生,减低了整个任务的执行效率。即便这样,实例c也比完全没有使用预读的实例a的任务执行效率还是要高一些。在实例c中,执行完毕该任务共需要34个时钟周期。当然我们还可以轻松出采用预读使图5‑1中的实例执行的更加缓慢。
图5‑1中的实例可以使用硬件预读的方式。但是无论采用什么方式,都需要注意预读的数据需要及时有效,而且在产生尽可能小的Overhead的基础上供微架构使用。在实例c的r1和r2中,预读操作过晚,因此指令流水依然会Stall,从而影响执行效率。
在r3中,预读操作过早,虽然数据可以提前进入某个Cache Block,但是这意味着过早预读的数据可能会将某个将要使用的Cache Block替换出去,因此CPU Core可能会重新读取这个被替换出去的Cache Block,从而造成了Cache Pollution。除此之外每一个Cache Block有自己的MLS,过早预读的数据,有可能被其他存储器访问替换出去,当CPU Core需要使用时,该数据无法在Cache中命中。
因此在进行数据预读时,需要首先重点关注时机,不能过早也不能过晚。如果考虑多处理器系统,无论是采用HB或者SD方式,做到恰到好处都是巨大的挑战。除了预读时机之外,需要进一步考虑,预读的数据放置到Cache Hierarchy的哪一级,L1,L2还是LLC,所预读的数据是私有数据还是共享数据。需要进一步考虑预读数据的Granularity,是By Word, Byte,Cache Block,还是多个Cache Block;需要进一步考虑是否采用HB和SD的混合方式。这一切增加了Prefetch的实现难度。
这也造成了在某些情况下,采用预读机制反而会降低效率。什么时候采用预读机制,关系到处理器系统结构的各个环节,需要结合软硬件资源统筹考虑,并不能一概而论。处理器提供了必备的软件和硬件资源以实现预读,如何“合理”使用预读机制是系统程序员考虑的一个细节问题。数据预读可以使用软件预读或者硬件预读两种方式实现,下文将详细介绍这两种实现方式。
软件和硬件预读策略所追求的指标依然是Coverage,Accuracy和Timeliness[101]。Coverage指CPU Core需要的数据有多少是从Prefetcher中获得,而不是访问存储器子系统;Accuracy指Prefetched Cache中有多少数据是CPU Core真正需要的;Timeliness指预读的数据是否能够恰到好处的到达,不能太早也不能太晚。
在已知的软件或者硬件预读策略主要针对以上三个参数展开,这些策略的底线是预读所使用的开销不大于于不使用预读机制时Cache Miss的开销。在许多情况下,采用预读策略不仅不会提高程序的执行效率,甚至会极大影响程序的正常执行,带来严重的系统惩罚,最终结果不如放弃这些预读机制。
我们需要对预读算法进行定性分析。假设Prefetch Ratio参数指由于Prefetch而读取的Cache Block总数在所有存储器访问的Cache Block中所占的比率;Transfer Ratio指Prefetch Ratio和Miss Ratio之和。
Access Ratio指所有Cache的访问次数与Prefetch Lookup之间的比值。所有Cache的访问次数是Actual和Prefetch Lookup之和。其中Prefetch Lookup指由Prefech算法决定当前Cache Block是否应该替换,是否应该Prefetch新的Cache Block而引发的Cache访问,是由Cache Controller主动发起的Cache访问操作;Actual Lookup指Cache Controller之外的访问操作,如CPU Core或者外部设备对Cache的访问操作。Access Ratio的值大于1。
在此基础之上,我们进一步引入参数D,P和A。其中参数D为Demand Miss所带来的Penalty,Demand Miss指没有采用预读而产生的Cache Miss开销;参数P为预读的代价,包括数据读入,因为读入新的数据而Replacement旧的数据,等各类因为预读导致的数据传递的开销;参数A为因为预读干扰了程序对Cache正常使用而带来的惩罚。在这种情况下,一个有效的预读算法需要满足公式5‑1。
![5.1 <wbr>数据预读 5.1 <wbr>数据预读](https://i-blog.csdnimg.cn/blog_migrate/5daf50d79d7056e2115aa01dd2a33191.jpeg)
如果出现Miss Ratio(Prefetch)大于Miss Ratio(Demand)的情况,即便P,A,Prefetch Ratio参数为0,上述公式也无法成立。这种情况是使用预读机制所造成的最糟糕结果。此时预读造成Cache Pollution,使得Cache Miss Ratio反而低于与没有使用预读的情况
硬件还是软件预读机制都会造成这种情况。与硬件预读相比,软件预读更加灵活一些。但是在很多情况之下,我并不喜欢使用编译器强行加入的预读处理,倾向根据微架构和应用的具体要求,书写这些预读代码。有时由编译器增加的预读代码除了进一步污染指令Cache之外,不会带来更多帮助。这不是否认编译器的努力,而是提醒读者需要因地制宜。
5.2 软件预读
软件预读机制由来已久,首先实现预读指令的处理器是Motorola的88110处理器,这颗处理器首先实现了Touch Load指令,这条指令是PowerPC处理器dcbt指令[4]的前身。后来绝大多数处理器都采用这类指令进行软件预读,Intel在i486处理器中使用Dummy Read指令,这条指令也是后来x86处理器中PREFETCHh[5]指令的雏形。
使用软件预读指令可以在处理器真正需要数据之前,向存储器预先发出读请求,这个预读请求不需要等待数据真正到达存储器之后,就可以执行完毕,以实现存储器访问与处理器运算同步进行,从而提高了任务的整体执行效率。
除了专有指令外,普通的读指令也可以用作预读,如Non-Blocking的Load指令。这个读指令与Prefetch指令最大的区别是,这些指令不仅将数据引入Cache层次结构,而且会将结果写入某个寄存器,这类指令也被称为Binding Prefetch。与此对应,在微架构中专门设置的Prefetch指令被称为Non-Binding Prefetch指令。
Prefetch指令需要采用Non-Blocking,Non-Exception-Generating方式实现。Non-Blocking较易理解,因为在一个使用Blocking Cache的微架构中,没有使用Prefetch指令的任何必要。在微架构中,一个简单实现Prefetch指令的做法是借用Non-Blocking load指令,并将结果传递给Nobody寄存器,较为复杂的实现是预读数据的同时,引入一些Hint,如微架构将如何使用预读的数据,是写还是读,这些信息有助于多核处理器的一致性处理。
Non-Exception-Generating指在Prefetch时不得引发Exception,包括Page Fault和其他各类的Memory Exception。在一些微架构中如果Prefech引发了Exception,获得的数据将被丢弃。此外Exception还会带来较大的Overhead,对Memory Consistency的实现制造障碍。
软件预读指令可以由编译器自动加入,但是在很多场景,更加有效的方式是由程序员主动加入预读指令。这些预读指令在进行大规模向量运算时,可以发挥巨大的作用。在这一场景中,通常含有大规模的有规律的Loop Iteration。这类程序通常需要访问处理较大规模的数据,从而在一定程度上破坏了程序的Temporal Locality和Spatial Locality,这使得数据预读成为提高系统效率的有效手段。我们考虑图5‑2中的实例。
![5.2 <wbr>软件预读 5.2 <wbr>软件预读](https://i-blog.csdnimg.cn/blog_migrate/cf59ddf38e47111b85b4cbb13fa76688.jpeg)
这个例子在进行向量运算时被经常使用,这段源代码的作用是将int类型的数组a和数组b的每一项进行相乘,然后赋值给ip,其中数组a和b的基地址Cache Block对界。我们假设N为一个较大的常数且能够被4整除,此外微架构的Cache Block为32字节,并在此基础上考虑图5‑2中的几个实例。
在实例a中没有使用预读机制进行优化。这段程序在执行时,a[i]和b[i]中的数据不会在处理器的Cache中命中,而且在顺序访问向量a和b的数据单元时,每次跨越Cache Block都会因为Compulsory Misses向存储器子系统发送读请求,从而stall微架构的指令流水,降低了程序的执行效率。
实例b在对变量ip赋值之前,首先对数组a和b进行预读,当对变量ip赋值时,数组a和b中的数据可能已经在Cache中,从而在一定程度上提高了代码的执行效率。这段代码并不完美。因为在绝大多数微架构中,预读以Cache Block为单位进行,对a[0], a[1], a[2], a[3]进行预读时都是对同一个Cache Block进行预读。因此这段代码对同一个Cache Block进行了多次预读,从而影响了执行效率。
实例c使用Loop Unrolling技术,将循环体内的赋值操作进一步展开为4个子步骤,从而避免了实例b中存在的多次预读。在现代处理器中,Branch Prediction较为完善,此处出现的Loop Unrolling并不会降低循环转移的开销,其主要目的是提高Cache Block的利用率,以减少预取次数。
实例d是在c基础上的继续优化,借用流水线设计的思想,将一次计算,分解为Prolog,Main loop和Epilog三个阶段。其中Prolog是建立流水时的准备工作,Main Loop是预读与计算的并行阶段,而Epilog是最后的结尾工作。
以上这些方法较为通用,有些编译器会自动将实例a转化为实例d。但是这些优化方式仍然忽略了一个细节,由于存储器的访问延时,预读的数据可能不会在计算需要时及时达到,指令流水线依然会Stall。为此预读指令需要进一步考虑存储器延时与计算所需时间之间的关系,保证预读的数据在计算需要时准时到达。
为此我们需要对Prefetch Distance参数做进一步分析,该参数简称为δ,其计算公式为δ = Ceiling(L/S)[100]。其中L为平均存储器访问延时,而S为一个Loop Iteration中计算部分使用的最短执行时间。
假设在实例d中,平均存储器访问延时为100个时钟周期,而一个Loop Iteration中的计算使用的最短执行时间为45个时钟周期时,δ参数的值为3。这一结果表明每次预读指令需要在3倍于Loop Iteration中的计算时间之前执行,才能保证软件流水可以顺利进行,不会因为预读的数据尚未到达而被迫等待。使用Prefetch Distance参数可以进一步优化实例d,如图5‑3所示。
![5.2 <wbr>软件预读 5.2 <wbr>软件预读](https://i-blog.csdnimg.cn/blog_migrate/ac307b377e6b515e365e5fe3fcf6a684.jpeg)
这些优化并不是软件预读的终点,还有很多利用某些Cache深层次特性做进一步优化的可能。这些优化都是具有一定的针对性,需要对处理器体系结构有着较为深刻的理解。在很多情况下软件预读机制有较为明显的缺点,首先是Code Expansion的问题,软件预读优化增加了代码长度,在一定程度上容易造成L1 Cache的Pollution,其次是预读指令本身的所带来的Overhead。采用硬件预读机制可以有效避免这两种缺陷,这使得更多的人开始重新关注硬件预读机制。
5.3 硬件预读
采用硬件预读的优点是不需要软件进行干预,不会扩大代码的尺寸,不需要浪费一条预读指令来进行预读,而且可以利用任务实际运行时的信息(Run Time Information)进行预测,这些是硬件预读的优点。
硬件预读的缺点是预读结果有时并不准确,有时预读的数据并不是程序执行所需要的,比较容易出现Cache Pollution的问题。更重要的是,采用硬件预读机制需要使用较多的系统资源。在很多情况下,耗费的这些资源与取得的效果并不成比例。
硬件预读机制的历史比软件预读更为久远,在IBM 370/168处理器系统中就已经支持硬件预读机制。大多数硬件预读仅支持存储器到Cache的预读,并在程序执行过程中,利用数据的局部性原理进行硬件预读。
最为简单的硬件预读机制是OBL(One Block Lookahead)机制,这种方式虽然简单,但是在许多情况下效率并不低于许多复杂的实现,也是许多处理器采用的方式。OBL机制有许多具体的实现方式,如Always prefetch,Prefetch-on-miss和Tagged prefetch[23]。
在使用Always Prefetch OBL实现方式时,当一段程序访问数据块b时,只要数据块b+1没有在Cache中Hit,就对数据块b+1进行预读。这种方式的缺点是可能程序访问数据块b之后,将很长时间不使用数据块b+1,从而带来较为严重的Cache Pollution。使用这种方式时的Access Ratio为2。
在使用Prefetch-on-Miss OBL实现方式时,当程序对数据块b进行读取出现Cache Miss时,首先将数据块b从存储器更新到Cache中,同时预读数据块b+1至Cache中;如果数据块b+1已经在Cache中,将不进行预读。使用这种方式时的Access Ratio为1+Miss Ratio。
Always Prefetch和Prefetch-on-Miss OBL方式没有利用之前的历史信息,在某些应用中,容易造成Cache Pollution。Tagged Prefetch是Prefetch-on-Miss实现方式的一种改进,其实现相对较为复杂,也使用了额外的硬件资源。
在使用Tagged Prefetch OBL实现方式时,需要为每一个Cache Block设置一个Tag位,该位在复位或者当前Cache Block被替换时设置为0。如果当前Cache Block是因为Prefetch的原因从其下的存储器子系统中获得时,该位依然保持为0。
当前Cache Block在预读后第一次使用,或者是Demand-Fetched时,Tag位将从0转换为1,此时如果其后的数据块不在Cache Block时将进行预读[23]。这种方式与Prefetch-on-Miss的最大区别在于访问已经Prefetch到Cache中数据的处理。
当程序访问已经预读到Cache的Block时,在使用Prefetch-on-Miss方式时,不会继续预读下一个Cache Block,而使用Tagged Prefetch方式时,会继续预读下一个Cache Block,从而减少了Demand-Fetched的概率,其实现示意如图5‑4。
![5.3 <wbr>硬件预读 5.3 <wbr>硬件预读](https://i-blog.csdnimg.cn/blog_migrate/dbd7839f475e9f4a26c80339f87b0037.jpeg)
从上图可以发现,对于一个顺序访问的Access Patern,使用Prefetch-on-Miss方式,每次访问过一个Prefetched Cache Block后,都会出现一次Cache Miss;而是用Tagged Prefetch时仅会出现一次Cache Miss。
但是仅用这一种访问模型,并不能证明Tagged Prefetch一定由于Prefetch-on-Miss方式。Alan J. Smith[23]根据Miss Ratio,Access Ratio和Transfer Ratio三个参数对以上实现方式进行了较为细致的对比。从Access Ratio参数的上看,Always prefetch实现方式大于后两种方式。
与Prefetch-on-miss方式相比,Tagged prefetch实现方式在Access Ratio和Transfer Ratio没有明显提高的前提下,降低了50%~90%的Miss Ratio [23]。但是我们依然不能得出Tagged prefetch一定优于Prefetch-on-miss方式的结论。与其他方式相比,Tagged Prefetch方式每一个Cache Block多使用了一个Tag位,依然是某种程度的Trade-off。
Tagged Prefetch实现有许多衍生机制,比如可以将数据块b+1,b+2,…,b+k预读到Cache中。其中k为Prefetch的深度,当k为1时,即为标准的Tagged Prefetch。更有甚者提出了一种Adaptive Sequential Prefetching实现方式,此时k可以根据任务执行的Run Time信息进行调整,可以为正,也可以为负。
以上这些硬件预读算法都有其局限性,特别是在处理Strided Array相关的计算时,为此也产生了一系列可以利用Stride信息的硬件预读实现,如Lookahead Data Prefetching实现[102]。该实现的组成结构如图5‑5所示。
![5.3 <wbr>硬件预读 5.3 <wbr>硬件预读](https://i-blog.csdnimg.cn/blog_migrate/091bb82e251a76eced4c1b5413bda96e.jpeg)
假设在一个3-Nested Loop Iterations中,某条存储器访问指令mi需要陆续访问a1, a2和a3。当(a2- a1) = Δ ≠ 0时,需要对mi进行预读,Δ参数即为预读的Stride。第一次预读地址A3 = a2 +Δ,其中A3为预测值,如果预测与实际的a3相同,则继续预测,直到An ≠ an。采用这种实现方法,需要使用历史地址信息和最后一次检测成功的Δ参数,为此在硬件上需要设置一个RPT(Reference Prediction Table),RPT的组成结构与Cache类似,如图5‑6所示。
![5.3 <wbr>硬件预读 5.3 <wbr>硬件预读](https://i-blog.csdnimg.cn/blog_migrate/fabc239b67d55fddb2bd3b48ca0ff79c.jpeg)
RPT由微架构的PC进行索引。当指令mi第一次执行时,将从RPT中分配一个空闲Entry,填写相应的Instruction Tag,Previous Address,并将state设置为initial状态。当指令mi第二次执行时,并在RPT中命中时,将根据当前的EA与Previous Address计算Atride参数后填入当前Entry,并将State设置为Transient状态。
此时如果地址(Effective Adderss+Stride)所指向的数据没有在Cache中命中,进行Tantative Prefetch操作。当指令mi第三次执行时,在RPT中命中,而且A3与实际的a3相同时,表示发生了一次Correct stride Prediction,此时继续进行下一个地址的预读,同时将State改写为Steady。在RPT中,State的状态迁移如图5‑7所示。
![5.3 <wbr>硬件预读 5.3 <wbr>硬件预读](https://i-blog.csdnimg.cn/blog_migrate/7cbe2a7b839421fe20f08aa825168a6b.jpeg)
根据图5‑7的状态迁移关系,我们考察以下实例,如图5‑8所示。其中左图为一个3-Nested Loop Iterations,并对数据a进行赋值操作,其中数组a,b和c使用的Stride参数并不相同。但是在一下程序中,数据a,b和c使用的Stride参数依然具有强烈的规律性,在RPT中分别保存着这些规律,从而在一定程度上提高了预读的准确性。
![5.3 <wbr>硬件预读 5.3 <wbr>硬件预读](https://i-blog.csdnimg.cn/blog_migrate/c7653a54ead19dac557d44d8722f733b.jpeg)
假设数组a, b和c的基地址分别以10,000, 20,000和30,000对界。在第一次进行运算时,通过计算可以在RPT表中记录相应的Previoud Address,数组a,b和c的Stride参数为初始值0,而State为初始状态Initial。
在第一次Iteration之后,RPT表中的数组b和c的Stride分别为4和400(Current Address与Previous Address之差),State改变为Transient,并开始预读之后的Cache Block,而通过计算数组a的Stride为0,与之前的值相同,State改变为Steady,即不进行预读;在第二次Iteration之后,RPT中的数组b, c和a发现Stride没有再次发生变化时,State改变为Steady,开始稳定地进行预读。
在第一重循环k执行完毕后,由于k的变化,将使RPT的数组c进入Initial状态,重新进入准备阶段;第二重循环j执行完毕后,由于j的变化,将使RPT的数组a和c进入Initial状态,重新进入准备阶段;第三重循环i执行完毕后,由于i的变化,将使RPT的数组a和b进入Initial状态,重新进入准备阶段。
周而复始,直到三重循环完全执行完毕。
采用这种硬件预读方法,可以有效解决在Loop Iterations中数据的Stride问题。在进一步考虑了Prefetch Distance,即δ参数的基础上,Lookahead data prefetching算法可以在此基础上继续优化,可以设置一个LA-PC(Lookahead Program Count)。此时预读的地址Prefetch Address等于Effective Addess + (Stride × δ),LA-PC与PC的差值即为δ。
在某些情况下,基于RPT的预读机制并不能理想地处理Triangle-Shaped Loop,这种Loop访问Stride值的计算不但与自身有关,而且与相邻的Loop直接相关。采用Correlated Reference Prediction预读机制[103]可以有效解决这一问题。
该机制的实现要点是除了关注在一个Loop内的数据访问轨迹之外,还关心相邻的Loop,以实现对Triangle-Shaped Loop的预读。为此在图5‑6中需要加入另外一组Prev Address和Stride参数,对此有兴趣的读者可参阅[103]以获得更详细的信息。
无论是软件还是硬件Prefetch的实现方式,都不可避免地出现Prefetch得来的数据并没有被及时使用,从而会在一定程度上一定程度上的重复,这种重复会进一步提高系统功耗,对于有些功耗敏感的应用,需要慎重使用Prefetch机制。Prefetch机制除了对系统有较大影响之外,还会引发一定程度的Cache Pollution。这使得Stream buffer[20]机制因此引入。
5.4 Stream Buffer
Stream Buffer是一种广义Cache,主要功能是避免因为预读而造成的Cache Pollution问题。当采用该机制时,处理器可以将预读的数据序列放入Stream Buffer中而不是放入Cache,如果处理器使用的数据没有在Cache中命中,将首先在Stream Buffer中查找,采用这种方法可以消除预读对Cache的污染,但是也因此增加了系统设计的复杂性。Stream Buffer的组成结构如图5‑9所示。
![5.4 <wbr>Stream <wbr>Buffer 5.4 <wbr>Stream <wbr>Buffer](https://i-blog.csdnimg.cn/blog_migrate/039ea141ce21a43dc64d4cb28a9656b8.jpeg)
在一个Stream Buffer中,由多个Entry组成,在这个Entry中可以存放一个或者多个Cache Block,也包含若干个状态位。Stream Buffer的每一个Entry由Cache Block,Valid位和与此对应的地址Tag组成。其中Valid位表示当前Cache Block中的数据是否有效,而地址Tag用来进行地址比较。Stream Buffer的使用方法与FIFO类似,从Front指针处开始使用,新的数据将填入Rear指针的位置。
出现Cache Miss时,微架构首先在Stream Buffer的Front开始寻找数据,如果命中,该数据才预读进入Cache,从而不会造成Cache Pollution,同时预读进行Cache的数据将从Stream Buffer的头部移除。随后微架构根据Prefetch Address从其下Cache Hierarchy中获得Cache Block,并填写Rear指针对应Entry的Tag信息,数据返回时将填写相应的Cache Block,并将Valid位置为有效。
如果数据在Stream Buffer中Miss,而且系统中只有一个Stream Buffer,该Stream Buffer将被刷新,并试图建立新的预读序列。显然在多数情况下,设立一个Stream Buffer并不合理,在一个实际的应用中,一个任务经常会访问多个Stride不同的数据序列,如图5‑8所示。为此在现代微架构中,一般设置多个Stream Buffer,即Multi-Way Stream Buffers,其组成结构如图5‑10所示。
![5.4 <wbr>Stream <wbr>Buffer 5.4 <wbr>Stream <wbr>Buffer](https://i-blog.csdnimg.cn/blog_migrate/07dbc6e8f1ba22ad663394b442582113.jpeg)
当出现Stream Buffer Miss时,将使用某种替换算法,LRU或者PLRU,替换其中的一个Stream Buffer,以装填新的访问序列。当使用这种结构时,如果一个任务需要访问Stride不同的几种数据序列时,可以使用不同的Stream Buffer,从而有效提高了Stream Buffer的利用率。在一个微架构的具体实现中还可以将Stream Buffer与Lookahead Data Prefetching方式联合使用,其结构示意如图5‑11所示。
![5.4 <wbr>Stream <wbr>Buffer 5.4 <wbr>Stream <wbr>Buffer](https://i-blog.csdnimg.cn/blog_migrate/7bf30d70c2d83d44c4501c99d0be2e7a.jpeg)
即便是使用这种硬件预读方式,也无法彻底解决因为预读带来的Cache Pollution问题,很难解决预读数据的及时有效等一系列。硬件预读机制不断的发展演变过程,与程序的分支预测有某些相近之处,其本质都是硬件自学习数据访问轨迹的过程。
各类Stride Prefeching,Distance Prefeching和Global Histrory Buffer Prefeching算法,其本质均是如此,没有必要对此再一一进行介绍。很多从Qualitative Research看起来非常不错的预读算法,其Quantitative Analysis的最终结果未必能够超过OBL算法。这些优化方法都有较强的针对性,在某类Access Pattern之下有较好的表现,而在其他情况之下并不适用。在Prefectch这个领域,有时简单逻辑获得的效果并不弱于复杂逻辑。
这也引发了一个思考,是否应该把更多的硬件资源用于微架构的其他部分,而非用于硬件预读。一些简单的方法可能就是最优,比如OBL实现和最基本的Stream Buffer。这一切依然是一个深层次的Trade-Off问题,没有优劣之分。
[1] 与[20]中的Stream Buffer示意图相比,[94]中的图片更为直观一些。
线束语
搁笔不意味着结束。许久之前,我与怀临先生聊过准备书写有关Cache的文字,这不是书写的目的,这篇文章与重然诺如邱山没有太多联系。心中想着《菜根谭》中的“宠辱不惊,闲看庭前花开花落;去留无意,漫随天外云卷云舒”,不知不觉完成了这些文字。
只是我依然尚不明了为何去写这些文字,不清楚如此惜寸阴,却花费了如此精力;不明了为何一直去在忽视,忍受着各种忽视去完成这篇文章。我奢求完成时可以发现少许原因。待到结束,却愈发模糊。
我们所处的年代与之前所有年代一样,总有些可以继承的事物。近些年我一直品读着这些事物,他们的尊严与智慧在历经时光磨砺后没有消失,而是加倍地尚显出来。这些可以被继承的事物并不是多数个体群体苛求的财富。
财富可以评价许多事物,就是不能评价生命为何高贵,就是不能让子孙后代去赖以自豪。堆积的财富终为土灰。卸任时留给美国政府最多财富的克林顿总统,在就职时曾说过一段话,When our founders boldly declared America's independence to the world and our purposes to the Almighty, they knew that America, to endure, would have to change. Not change for change's sake, but change to preserve America's ideals; life, liberty, the pursuit of happiness. Though we march to the music of our time, our mission is timeless.
使命没有高下之分,都是为尊严而战。尊严很贵,不能去乞讨,更没有人会给予你,只有赚足了本钱,一口气赢回来。这种本钱并不是财富。可以富可敌国依然无法赢得士人之心,古已有之。大人物有其使命,小个体有自己的追逐,没有高下之分。
圣经中有段话“And let us not be weary in well-doing, for in due season, we shall reap, if we faint not”,翻译成中文是“我们努力,不求回报,时候到了,就有收成”。我明白没有什么特别的目的驱使我完成这些文字。
真放肆不在饮酒高歌。兴之所至,无处不是乐土。我安于在这条轨迹中前行,只要前方有路,不在乎路途遥远。这次书写,比之前的完成的所有文章难出许多,每次在获得少许的进展后,发现的是更多的无知。我喜欢这种无知。近来多读《坛经》,以其中的一句话作为全文的结束。
世界虚空,能含万物色像。日月星宿、山河大地、泉源溪涧、草木丛林、恶人善人、恶法善法、天堂地狱、一切大海、须弥诸山,总在空中。