第2章 Cache 摘录

2.1 Cache的一般设计

在超标量处理器中,有两个部件直接影响着性能,即分支预测Cache。纵观每次Intel处理器升级,都会使分支预测的精度更高,Cache的命中率更高。

Cache之所以存在,是因为在处理器在计算机的世界中,存在如下两个现象:
        (1) 时间局部性(temporal locality):如果一个数据现在被访问了,那么在以后很有可能还会被访问。
        (2) 空间局部性(spatial locality):如果一个数据现在被访问了,那么它周围的数据在以后有可能也会被访问。

Cache的出现可以说是一种无奈的妥协,因为存储器技术的发展比处理器技术的发展要慢。

尤其在超标量处理器中,考录到每周期需要从Cache中同时读取多条指令,同时每周期也可能有多条load/store指令会访问Cache,因此需要实现多端口的Cache,这给芯片面积的速度带来了不小的挑战。

现代的超标量处理器都是哈佛结构,为了增加流水线的执行效率,L1 Cache一般都包括两个物理部分,指令Cache(I-Cache)和数据Cache(D-Cache),本质来说,他们的原理都是一样的,但是D-Cache不仅需要读取,还需要考虑写入的问题,而I-Cache只会被读取,并不会被写入,因此D-Cache更复杂一点。

L1 Cache更靠近处理器,它是流水线的一部分,需要保持与处理器近似的速度,这注定了它的容量不能够很大。L1 Cache一般是SRAM来实现的,容量大的SRAM需要更长的时间来找到一个指定地址的内容,但是一旦不能和处理器保持速度上近似,L1 Cache就失去的了意义。对于L1 Cache而言,快就是硬道理

L2 Cache则是为了求“全”。一般情况下,L2 Cache都是指令和数据共享,它不必保持处理器的步调,可以容忍更慢一些,它的主要功能是为了尽量保存更多的内容。目前L2 Cache都是MB为单位的。在多核之间共享的L2 Cache就更复杂一点,现在处理器一般都是共享L3 Cache。L1 Cache依旧是每个核“私有”的。

在超标量处理器中,对于Cache有着特殊的要求。对于I-Cache来说,需要能够每周期读取多条指令,不过,它的延迟时间即使很大,在一般情况下也不会造成处理器性能的下降,仍旧可以实现每周期都可以读取指令的效果。除非遇见预测跳转的分支指令时,这些延迟才会对性能造成影响。

对于D-Cache来说,它需要支持在每周期内多条load/store指令的访问,也就是需要多端口的设计。虽然在超标量处理器中,多端口的部件很多,例如发射队列Issue Queue,Store Buffer,寄存器堆Register File和重排序缓存ROB等等,但是这些部件的容量本身就很小,所以即使采用多端口设计,也不会占用很大的空间。而D-Cache容量本身就很大,如果采用多端口设计,则占用的硅片面积就很难接受,也会导致更大的延迟,这个延迟会直接暴露给流水线中后续的指令。一般load处于相关性的顶端,所以这会对处理器的性能造成负面的影响。而对于L2 Cache而言,它的被访问频率不是很高(L1 Cache的命中率是比较高的),所以它不需要多端口的设计,它的延迟也不是特别的重要。因为只有发生L1 Cache miss的时候才会访问它。但是L2 Cache需要比较高的命中率,因为在它发生缺失时候会去访问物理内存DRAM,这个访问时间会很长,因此尽量提高L2 Cache的命中率。

Cache主要由两部分组成,Tag部分和Data部分。

Data部分保存一片连续地址的数据,而Tag部分则是存储着这片连续数据的公共地址。

一个Tag和它对应的所有数据组成的一行被称为一个Cache line。

而Cache line中的数据部分称为数据块(Cache data block)。

如果一个数据可以存储在Cache中的多个地方,这些被同一个地址找到的多个Cache line称为Cache Set。

实际中,Cache有三种主要的实现方法,直接映射direct-mapped,组相连set-associative和fullt-associative Cache。

对于一个物理内存而言,如果在Cache中只有一个地方可以容纳它,它就是直接映射的Cache;如果有多个地方都可以放置这个数据,它就是组相连的Cache;如果Cache中任何地方都可以放置这个数据,那么它就是全相连的Cache。

直接相连和全相连实际是组相连Cache中的特殊情况。现代Cache一般属于上述三种方式中的某一个,例如TLB和Victim Cache多采用全相连结构,而普通的I-Cache和D-Cache则采用组相连结构等。

Cache只能保存最近被处理器使用过的内容,由于它的容量有限,很多情况下,要找的指令或者数据并不在Cache中,这称为Cache的缺失(Cache miss)。在计算领域,影响Cache缺失的情况可以概括为:
        (1)Compulsory,由于Cache只是缓存以前访问过的内容,第一次被访问的指令或数据肯定就不会在Cache中,当然可以采用预取prefetch的方法来尽量降低这种缺失发生的频率。
        (2)Capcacity,Cache容量越大,就可以缓存更多的内容,因此容量是影响Cache缺失发生频率的一个关键因素。例如当程序频繁使用的5个数据属于不同的Cache set时,而Cache的容量只有4个Cache Set。
        (3)Conflict,一般使用组相连结构的Cache来减少多个数据映射到Cache中同一个位置的情况。当然考虑到实际硅片面积的限制,相连度不可能很高。例如,在一个有着两路结构(2-way)的Cahce中,如果程序频繁使用的三个数据属于同一个Cache set,那么就肯定会发生缺失,此时可以使用Victim Cache来缓解这个问题。

尽管在超标量处理器中,可以采用预取prefetch和victim cache这两种方法,但是仍然无法从根本上消除Cache缺失,只能尽量减少它发生的频率而已。

2.1.1 Cache的组成方式

1. 直接映射

直接映射(direct-mapped)结构的Cache是最容易实现的一种方式,处理器访问存储器的地址会被分为三部分,Tag、Index和Block Offset。使用Index来从Cahce中找到一个对应的Cache lline (set此时就是line),但是所有的Index相同的地址都会寻址到这个Cache line,因此Cache line中还有Tag部分,用来和地址中的Tag进行比较,只有它们是相等的,才表明这个Cache line就是想要的那个。在一个Cache line中有很多个数据,通过存储器地址中的Block Offset部分可以找到真正想要的数据,它可以定位到每个字节。在Cache line中还有一个有效位valid,用来标记这个Cache line是否保存着有效的数据,只有在之前被访问过的存储器地址,它的数据才会存在于对应的Cache line中,相应的有效位也会被置为1了。

对于所有Index相同的存储器地址,都会寻址到同一个Cache line中,这就会发生冲突,这也是直接映射结构Cache的一大缺点。如果两个Index相同的存储器地址交叉访问Cache,就会一直导致Cache缺失,严重地降低了处理器的执行效率。

下面通过一个实际的例子来说明存储器地址的划分对于Cache的影响,如图所示一个32位存储器地址,Block Offset有5位,所以Data block的大小是2^5 = 32字节,Index是6位,表示Cache中共有2^6=64个Cache line(在直接映射中,Cache line和Cache Set是同义的),存储器地址中剩余的32-5-6=21位作为Tag值,因此这个Cache中可以存储的数据大小为64*32=2048字节,即2KB;而Tag部分的大小是64*21=1344位,约为1.3Kb;有效位占用的大小为64*1=64位,一般情况下,都是以数据部分的大小来表示Cache的大小,因此这个Cache被称为一个2KB直接映射结构的Cache,而实际它还额外占用多余1.3KB的存储空间

直接映射结构的Cache在实现上是最简单的,它都不需要替换算法,但是它的执行效率也是最低的,现代的处理器很少使用这种方式了。

2.组相连

组相连(set-associative)的方式是为了解决直接映射结构Cache的不足而提出的。存储器中的一个数据不单单只能放在一个Cache line中,而是可以放在多个Cache line中,对于一个组相连结构的Cache来说,如果一个数据可以放在n个位置,则称这个Cache是n路组相连(n-way)。

这种结构依旧使用存储器地址的Index部分对Cache进行寻址,此时可以得到n个Cache line,这两个Cache line称为一个Cache set,究竟哪个Cache line才是最终需要的,是根据Tag比较的结果来确定的,当然,如果n个Cache line的Tag比较结果都不相等,那么发生了Cache缺失。

因为需要从多个Cache line中选择一个匹配的结果,这种Cache的实现方式较之直接映射结构的Cache,延迟会更大,有时候甚至需要将其进行流水线,以便减少对处理器周期时间cycle time的影响,这样会导致load指令的延迟增大,一定程度上影响了处理器的执行效率,但是这种方式的优点也很突出,它可以显著减少Cache缺失发生的频率,因此在现代处理器中得到了广泛的应用。

上面提到的Tag和Data部分可以分开放置,称为Tag SRAM和Data SRAM,可以同时访问这两个部分,称为并行访问;相反如果先访问Tag SRAM部分,根据Tag比较的结果再去访问Data SRAM部分,称为串行访问。

对于并行访问结构,当Tag部分的某个地址被读取的同时,这个地址在Data部分对应的所有数据也会被读取出来,并送到一个多路选择器上,这个多路选择器受到Tag比较结果的控制,选出相应的Data block,然后根据存储器地址中的Block offset的值,选择出合适的字节,一般将选择字节的这个过程称为数据对齐data alignment。

并行访问Cache若在一个周期内完成,则在现实当中会占据很大延迟,要想使处理器运行在较高的频率下,Cache的访问就需要使用流水线,前面说过,对于指令Cache来说,流水线的结构不会有太大的影响,仍旧可以实现每周期读取指令的效果;而对于数据Cache来说,使用流水线则会增大load指令的延迟,从而对处理器的性能造成负面影响。

流水线的地址计算Address Calculation阶段可以计算得出存储器的地址,接下来的Disambiguation阶段对load/store指令之间存在的相关性进行检查,然后在下个流水线阶段Cache Access就可以直接并行地访问Tag SRAM和Data SRAM,并使用Tag比较的结果对输出的数据进行选择,然后在下一个流水线阶段Result Derive,使用存储器地址中的block offset值,从数据部分给出的data block中选出最终需要的数据。将整个Cache的访问放到几个周期内完成,可以降低处理器的周期时间。

而对于串行访问方法来说,首先需要对Tag SRAM进行访问,根据Tag比较的结果,就可以知道数据部分中的那一路是需要被访问的,此时可以直接访问这一路的数据,不需要上图中的多路选择器了,而且只需访问数据部分指定的那个SRAM,其他的SRAM由于都不需要被访问,可以将它们的使能信号置为无效,这样可以节省很多功耗

完全串行Tag SRAM和Data SRAM这两部分的访问,它的延迟会更大,仍旧需要使用流水线的方式来降低对处理器的周期时间的影响,用流水线降低了访问Tag SRAM和Data RAM的延迟,因为此时已经不再需要多路选择器,这对降低处理器的周期时间是有好处的,但是这样设计的一个明显的缺点就是Cache的访问增加了一个周期,这也就增大了load指令的延迟,因为load指令处于相关性的顶端,会对处理器的执行效率造成一定的负面影响。

并行访问的方式会有较低的时钟频率和较大的功耗,但是访问Cache的时间周期缩短了一个周期,但是乱序执行的超标量处理器可以将访问Cache的这段时间,通过填充其他的指令而掩盖起来,所以对于超标量处理器来说,当Cache的访问处于关键路径上时,可以采用串行访问来提高处理器的时钟频率,同时并不会由于访问Cache的时间增加了一个周期而引起性能的明显降低;相反,对于普通的顺序执行的处理器来说,由于无法对指令进行调度,访问Cache如果增加了一个周期,就很有可能会引起处理器性能的降低,因此在这种处理器中使用并行访问的方式就是一种比较合适的选择。

3.全相连

在全相连full-associative的方式中,对于一个存储器地址来说,它的地址可以放在任意一个Cache line中。存储器地址不再有Index部分,而是直接在整个Cache中进行Tag比较,找到比较结果相等的那个Cache line,相当于直接使用存储器的内容来寻址,从存储器中找到匹配的项,这其实就是内容寻址的存储器Content Address Memory,CAM。实际当中的处理器在使用全相连结构的Cache时,都是使用CAM来存储Tag值,使用普通的SRAM来存储数据。全相连结构Cache有着最大的灵活度,它的延迟也是最大的,因此一般这种结构的Cache不会有很大的容量,例如TLB就会使用这种全相连的方式来实现。

2.1.2 Cache的写入

在一般的RISC处理器中,L1 Cache中的I-Cache都是不会被直接写入内容的,即使有自修改的情况出现,也并不是直接写入I-Cache,而是借助与D-Cache来实现,将要改写的指令作为数据写到D-Cache中,然后将D-Cache中的内容写入到下级存储器,例如L2 Cache。

对于D-Cache来说,它的写操作和读操作有所不同,当执行store指令时,如果只是向D-Cache中写入数据,而并不改变它的下级存储器中的数据,这样就会导致D-Cache和下级存储器中,对于这一个地址有着不同的数据,这称为不一致non-consistent。

要想保持一致性,最简单的方式就是当数据在写到D-Cache时,也写到它的下级存储器中,这种方式称为write-through写通。优点是保持数据一致性,缺点是处理器执行效率不高。

如果在执行store指令时,数据被写到D-Cache后,只是对被写入的Cache line做一个记号,并不将这个数据写入到更下级的存储器中,只有当Cache中这个被标记的line要被替换时,才将它写到下级存储器中,这种方式被称为Write back写回,被标记的记号在计算机中术语为dirty状态。优点可是减少写频率,缺点就是造成D-Cache和下级存储器中有很多地址中的数据是不一致的,给存储器的一致性管理带来负担,以及设计复杂度高一点。

以上是假设写D-Cache的地址总是在D-Cache中,而实际当中,有可能发现这个地址并不在D-Cache中,这就发生了写缺失write miss,此时最简单的处理方式就是将数据直接写到下级存储器中,而不是写到D-Cache中,这种方式称为non-write allocate。与之对应为write allocate,如果写Cache时发生了缺失,会首先将下级存储器中将这个发生缺失的地址对应的数据块data block读取出来,将要写入到D-Cache中的数据合并到这个数据块,然后将这个被修改过的数据块写到D-Cache。

此处的一个疑问,当写D-Cache发生缺失时,为什么不直接在D-Cache中找到一个line,将要写入的信息直接写到这个line中,同时也将它写到下级存储器中呢?为什么还要先从下级存储器中读取相对应的数据块并写到D-Cache中?

这是因为在处理器中,对于写D-Cache来说,最多也就是写入一个字,而如果按照上面方式,直接从D-Cache中找到一个line来存储这个需要写入的数据,并将这个line标记为dirty,那么就会导致这个line中,数据块的其他部分和下级存储器中对应的地址的数据不一致,而且此时D-Cache中这些数据都是无效的,如果这个Cache line由于被替换而写回到下级存储器时,就会使下级存储器中的正确数据被篡改。

对于Write allocate方法来说,就需要在发生写缺失时,首先将缺失的地址对应的数据块从下级存储器中读取出来,这个过程是必不可少的。

对于D-Cache来说,一般情况下,write through是和non-write allocate一起使用的,它们都是直接将数据更新到下级存储器中。

1.write through和non-write allocate配合工作流程图:

write-back是和write allocate配合使用的,不管是读取还是写入时发生缺失,都需要从D-Cache中找到一个line来存放新的数据,这个被替换掉的line如果是dirty,那么首先需要将其中的数据写回到下级存储器中,然后才能使用这个line存放缺失地址对应的数据块。也就是说,当D-Cache中被替换的line是dirty,需要对下级存储器进行两次访问。对于写D-Cache的操作来说,还需要将写入的数据也放到这个line中,并将其标记为dirty。

2.write-back与write allocate工作流程图:

在D-Cache中采用write back方法时,不管是读取还是写入时发生缺失,都需要从D-Cache中找到一个line来存放新的数据,这个被替换的line状态如果是dirty,那么先将其中的数据写回到下级存储器中,然后才能够用这个line存放新的数据。

2.1.3 Cache的替换策略

不管是读取还是写入D-Cache时发生了缺失,都需要从有效的Cache line中找到一个并替换之,这就是替换Cache replacement策略。

1.近期最少使用法

近期最少使用法Least Recently Used,LRU会选择最近被使用次数最少的的Cahce line,因此这个算法需要跟踪每个Cache libe的使用情况,为每个Cache line都设置一个年龄age部分,每次当一个Cache line被访问时,它对应的年龄部分就会增加,或者减少其他Cache line的年龄值,这样当进行替换时,年龄值最小的那个Cache line就是被使用次数最少了,会选择它进行替换。但是随着Cache相关联度增加,要精确实现这种LRU方式就非常昂贵了。因此在way很多的情况下,都是使用伪LRU的方法。

在下面8-way组相连Cache中,对所有way进行分组,共有三部分,分别介绍如下;
(1)首先将所有way分为两组,每组有4个way;
(2)然后将每组中的way在分为两组,也就是每组有2个way;
(3)继续进行分组,此时每组只有一个way了

2.随机替换

在处理器中,Cache的替换算法一般都是使用硬件来实现的,因此如果做得很复杂,会影响处理器的周期时间,于是就有了随机替换Random replacement的实现方法,此算法无需记录每个way的年龄信息,而是随机地选择一个way进行替换,相比于LRU替换方法来说,这种方法发生缺失的频率会更高一些,但是随着Cache容量的增大,这个差距是越来越小的。实际设计很难做到严格的随机,一般采用一种称为时钟算法clock algorithm的方法来实现近似的随机,工作原理本质上是一个计算器,计数器的的宽度由Cache的相关度,也就是way的个数来决定,例如8way就需要三位,每次当Cache中的某个line需要被替换时,就会访问这个计数器,使用计数器当前的值,从被选定的Cache set找出要替换的line,这样就近似地实现一种随机的替换。理论上其性能不是最优,但是它的硬件复杂度比较低,也不会损失过多的性能。

 2.2 提高Cache的性能

在真实世界的处理器中,会采用更复杂的方法来提高Cache的性能,这些方法包括写缓存write buffer、流水线pipeline cache、多级结构multilevel cache、victim cache和预取prefetch。除此之外,对于乱序执行的超标量处理器来说,根据它的特点,还有一些其他方法来提高Cache的性能,例如非阻塞non-blocking cache、关键字优先critical word first和提前开始early restart方法。

2.2.1 写缓存

不管是load或者store指令,当D-Cache发生缺失时,需要从下一级存储器中读取数据,并写到一个选定的Cache line中,如果这个line是dirty,那么首先需要将它写到下级存储器中,考虑下级存储器,例如L2 Cache或是物理内存,一般只有一个读写端口,这就要求上面的过程是串行完成的。先将dirty的Cache line的数据写到下级存储器中,然后才能读取下级存储器,得到缺失的数据,由于下级存储器的访问时间都比较长,这种串行的过程导致D-Cache发生缺失的处理时间变得很长,此时就可以采用write buffer写缓存来解决这个问题,dirty的Cache line首先放到写缓存中,等到下级存储器有空闲的时候,才会将写缓存中的数据写到下级存储器中。

对于write back类型的D-Cache来说,当一个标记为dirty的Cache line被替换的时候,这个line中的数据会首先放到写缓存中,然后就可以从下级存储器中读数据了。

对于write through类型的D-Cache来说,采用写缓存之后,每次当数据写到D-Cache的同时,并不会同时写到下级存储器中,而是将其放到写缓存中,这样就减少了write through类型的D-Cache在写操作时需要的时间,从而提高了处理器的性能,以及write through类型的Cache由于便于进行存储器一致性的管理,所以在多核的处理器中,L1 Cache会经常采用这种结构

加入写缓存之后,会增加系统设计的复杂度,举例来说,当读取D-Cache发生缺失时,不仅需要从下级存储器中查找这个数据,还需要在写缓存中也进行查找。

写缓存就相当于是L1 Cache到下级存储器之间的一个缓冲,通过它,向下级存储器中写数据的动作会被隐藏,从而可以提升处理器的执行效率,尤其是对于write through类型的D-Cache而言。

2.2.2 流水线

对于读取D-Cache来说,由于Tag SRAM和Data SRAM可以在同时进行读取,所以当处理器的周期时间要求不是很严格时,可以在一个周期内完成读取的操作;而对于写D-Cache来说,情况就比较特殊了,读取Tag SRAM和写Data SRAM的操作只能串行地完成。只有通过Tag比较,确认需要写的地址在Cache中之后,才可以写Data SRAM,在主频比较高的处理器中,这些操作很难在一个周期内完成。这就需要对D-Cache的写操作采用流水线的结构。比较典型的方式是将Tag SRAM的读取和比较放在一个周期,写D-Cache放在下一个周期。

需要注意当执行load指令时候,它想要的数据可能正好处于store指令的流水线寄存器中,而不是来自于Data SRAM,因此需要机制能检测到这种情况,将load指令所携带的地址和store指令的流水线寄存器进行比较。

对写D-Cache使用流水线之后,不仅增加了流水线本身的硬件,也带来一些额外的硬件开销。

2.2.3 多级结构

现代处理器很渴望有一种容量大,同时速度又很快的存储器,但在硅工艺条件下,对存储器来说,容量和速度是一对相互制约的因素,容量大必然速度慢。

为了使处理器看起来使用了一个容量大同时速度快的存储器,可以使用多级结构的Cache

一般情况下,L1 Cache的容量很小,能够和处理器保持在同样速度等级上,L2 Cache的访问通常需要消耗处理器的几个时钟周期,但是容量要更大一些,L1和L2 Cache都会和处理器放在同一芯片上,现在L3 Cache也放在片上。

一般在处理器中,L2 Cache会使用write back方式,但是L1 Cache更倾向采用wirte through,这样可以简化流水线设计,尤其在多核情况下,管理存储器之间的一致性。

对于多级结构的Multilevel Cache,还需要了解两个概念,Inclusive和Exclusive:

Inclusive:如果L2 Cache包括了L1 Cache的所有内容,则称L2 Cache是Inclusive;

Exclusive:如果L2 Cache与L1 Cache的内容互不相同,则称L2 Cache是Exclusive;

Inclusive类型的Cache是比较浪费硬件资源的,因为它将一份数据保存在两个地方,优点则是可以直接将数据写到L1 Cache中,虽然此时会将Cache line中原来的数据覆盖掉,但是在L2 Cache中存有这个数据的备份,所以这样的覆盖不会引起任何问题(当然,被覆盖的line不能是dirty),以及也简化了一致性coherence的管理。

例如在多核的处理器中,执行store指令改变了存储器中的一个地址的数据时,如果是Inclusive类型的Cache,那么只需检查最低一级的Cache即可(L2 Cache),避免打扰上级Cache(L1 Cache)和影响处理器流水线;

如果是Exclusive类型的Cache,很显然要检查所有的Cache,而检查L1 Cache也就意味着干扰了处理器的流水线。如果处理器要读取的数据不在L1 Cache中,而在L2 Cache中,那么在将数据从L2 Cache放到L1 Cache的同时,也需要将L1 Cache中被覆盖的数据写到L2 Cache中,这样数据交换很显然会降低处理器的效率,但是Exclusive类型的Cache避免硬件的浪费,可以获得更多可用的容量。

2.2.4 Victim Cache

Cache中被“踢出”的数据可能马上又要被使用,因为Cache中存储的是经常要使用的数据。例如对一个2-way组相连的D-Cache来说,如果数据频繁使用的3个数据恰好都位于同一个Cache set中,那么就会导致一个way中的数据经常被“踢出”Cache,然后又经常地被写回Cache。

这会导致Cache始终无法驻留需要的数据,显然降低了处理器的执行效率,如果为此增加Cache中的way个数,又会浪费大量的空间。Victim Cache正是要解决这样的问题,它可以保存最近被踢出Cache的数据,因此所有的Cache set都可以利用它来提高way的个数,通常Victim Cache采用全相连的方式,容量都比较小(一般存储4~16个数据)。 

Victim Cache本质上相当于增加了Cache中way的个数,能够避免多个数据竞争Cache中有限的位置,从而降低了Cache的缺失率。一般情况下,Cache和Victim Cache存在互斥关系,也就是它们不包含同样的数据,处理器内核可以同时读取它们。

同样,Victim Cache的数据会被写到Cache中,而Cache中被替换的数据会写到Victim Cache中,这就相当于它们互换了数据。

还有一种跟Victim Cache类似的设计思路,称为Filter Cache,只不过使用在Cache之前,而Victim Cache使用在Cache之后,当一个数据第一次使用时,它并不会马上放到Cache中,而是首先会被放到Filter Cache中,等到这个数据再次被使用时,它才会被搬移到Cache中,这样做可以防止那些偶然使用的数据占据Cache。

 2.2.5 预取

影响Cache缺失率的3C中一项为Compulsory,当处理器第一次访问一条指令或者一个数据时,这个指令或数据肯定不会在Cache中,这种情况引起的缺失似乎不可避免,但是实际上使用预取prefetch可以缓解这个问题。所谓预取,本质上也是一种预测技术,它猜测处理器在以后可能会使用什么指令或数据,然后提前将其放到Cache中,这个过程可以使用硬件或者软件完成。

1.硬件预取

对于指令来说,猜测后续会执行什么指令相对比较容易,因为程序本身是串行执行的,虽然由于分支指令的存在,这种猜测有时候也会出错,导致不会被使用的指令进入了I-Cache,这一方面降低了I-Cache实际可用的容量,一方面又占用了本来可能有用的指令,这称为“Cache污染”,不仅浪费了时间,还会影响处理器的执行效率,为了避免这种情况,可用将预取的指令放到一个单独的缓存中。

当I-Cache发生缺失时,处理器将需要的数据块data block从下级存储器取出并放到I-Cache中,还会将下一个数据块也读取出来,只不过它不会放到I-Cache中,而是放到Stream Buffer的地方。在后续执行时,如果在I-Cache中发生了缺失,但是在Stream Buffer找到了想要的指令,那么除了使用Stream Buffer中读取的指令之外,还会将其中对应的数据块搬移到I-Cache中,同时继续从下一级存储器中读取下一个数据块放到Stream Buffer,当程序中没有遇到分支指令时,这种方法会一直正确地工作,从而使I-Cache的缺失率得到降低。但是分支指令会导致Stream Buffer的指令变得无效,此时的预取相当于做了无用功,浪费了总线带宽和功耗。预取是一把双刃剑,它可能会减少Cache的缺失率,也可能由于错误地预取而浪费了功耗和性能。

不同于指令的预取,对于数据的预取来说,它的规律更难以进行捕捉。一般情况下,当访问D-Cache发生缺失时,除了将所需要的数据块从下级存储器中取出来之外,还会将下一个数据块也读取出来。Intel Pentium 4和IBM Power5处理器中,采用了一种称为Strided Prefetch方法,它能够使用硬件来观测程序中使用数据的规律。

2. 软件预取

使用硬件进行数据的预取,很难得到满意的结果,其实在程序的编译阶段,编译器complier就可以对程序进行分析,进而知道哪些数据是需要进行预取的,如果在指令集中有预取指令prefetch instruction,那么编译器就可以可以直接控制程序进行预取。

应用软件预取方法有一种前提,就是预取的时机。如果预取数据的时间太晚,那么当真正需要使用数据时,有可能还没有被预取出来,这样预取就失去的意义;如果预取的时间太早,那么就有可能踢掉D-Cache中一些本来有用的数据,造成Cache污染。

还需要注意,使用软件预取的方法,当执行预取指令的时候,处理器需要能够继续执行,也就是能继续从D-Cache中读取数据,而不能让预取指令阻碍了后面指令的执行,这要求D-Cache是non-blocking结构。

在实现虚拟存储器Virtual memory系统中,预取指令有可能会引起一些异常exception发生,例如Page fault、虚拟地址错误virtual address fault或者保护违例Protection Violation。此时若对异常进行处理,就称其为处理错误的预取指令Faulting Prefetch Instruction,反之,称为不处理错误的预取指令nonfaulting prefetch instruction。

2.3 多端口Cache

在超标量处理器中,为了提高性能,处理器需要能够在每周期同时执行多条load/store指令,这需要一个多端口的D-Cache,以便支持多条load/stroe指令的同时访问

其实在超标量处理器中,有很多重要部件都是多端口结构的,比如寄存器堆register file、发射队列issue queue和重排序缓存ROB等。由于这些部件本身容量不是很大,所以即使采用多端口结构,也不会对芯片的面积和速度产生太大的影响,但是D-Cache不同,它的容量本身就很大,如果采用多端口设计,会有很大负面影响,因此需要采用一些办法来解决这个问题,本节重点介绍True Multi-port、Multi Cache Copies和Multi-banking。

2.3.1 True Multi-port

虽然在现实中,不可能对Cache直接采用多端口设计,但是本节还是看一下这种最原始的方法究竟有何缺点,这种方法使用一个多端口的SRAM来实现多端口的Cache,以一个双端口的Cache为例,所有在Cache中的控制通路和数据通路都需要进行复制,这表示它有两套地址解码器address decoder,使两个端口可以同时寻址Tag SRAM和Data SRAM;有两个多路选择器way mux,用来读取两个端口的数据;比较器的数量也需要增加一倍,用来判断两个端口的命中情况:同时还需要有两个对齐器aligner等。Tag SRAM和Data SRAM本身并不需要复制一份,但是它们当中的每个Cell都需要支持两个并行的读取操作,但是不需要两个写端口,因为无法对一个SRAM Cell同时写两次。

此种方法需要将很多电路进行复制,因此增大了面积,且SRAM Cell需要驱动多个读端口,因此需要更长的访问时间,功耗也随之增大,所以一般不会直接采用这种方式来设计多端口Cache。

2.3.2 Multiple Cache Copies

将Tag SRAM和Data SRAM进行了复制,与2.3.1节类似,不过是将Cache进行复制,SRAM将不再需要使用多端口的结构,这样可以基本上消除对处理器周期时间的影响。但是,这种方法浪费了很多面积,而且需要保持两个Cache间的同步。例如store指令需要同时写到两个Cache中,当一个Cache line被替换,也需要对另一个Cache进行同样的操作,此设计显然非常麻烦,不是一个很优化的方法,在现代处理器中很少被使用。

2.3.3 Multi-banking

此结构是在现实当中的处理器被广泛使用的方法,它将Cache分为很多小个bank,每个bank都只有一个端口,如果在一个周期之内,Cache的多个端口上的访问地址位于不同的bank之中,那样就不会引起任何问题,只有当两个或多个端口的地址位于同一个bank之中时,才会引起bank conflict。

使用这种方法,一个双端口的Cache仍旧需要两个地址解码器、两个多路选择器、两套比较器和两个对齐器,而Data SRAM此时不需要多端口结构了,这样就提高了速度,并在一定程度上减少了面积。但是由于需要判断Cache的每个端口是否命中,所以对于Tag SRAM来说,仍旧需要提供多个端口同时读取的功能,也就是采用多端口SRAM来实现。

影响这种多端口Cache性能的一个关键因素就是bank冲突,可以采用更多的bank来缓解这个问题,使bank冲突发生的概率尽可能降低,并且还可以提高bank的利用效率,避免有用的数据都集中在一个bank的情况发生,同时,由于每个端口都会访问所有bank,这需要更多的布线资源,有可能对版图设计造成一定的影响。

2.3.4 真实的例子AMD Opteron的多端口Cache

AMD的Opteron系列处理器是64位处理器,但是考虑到现实的需求,处理器的地址并没有使用64位,它的虚拟地址virtual address是48位,物理地址physical address是40位,采用简化地址从而减少硅片面积。

Opteron处理器的D-Cache是双端口的,每个端口都是64位的位宽,双端口以为这这个Cache能够在一个周期内支持=两条load/store指令同时进行访问,它使用了multi-banking的机制。

在AMD Opteron处理器的这个Cache中,data block的大小是64字节,需要6位地址进行寻址,每个data block被封为8个独立的bank,每个bank都是64位的单端口SRAM。

整个Cache的大小是64KB,采用2-way组相连,因此每一路的大小是32KB;使用Virtually-index,physically-tag的实现方式,直接使用VA虚拟地址来寻址Cache。因为每一路是32KB大小,因此需要15位地址寻址,又因为每个data block大小是64字节,因此寻址其中的每个字节需要使用VA[5:0],剩下的VA[14:6]用来寻找每个Cache set。

由于每个Cache line中的data block被划分为8个bank,每个bank是8字节宽的SRAM,所以很自然地使用VA[5:3]来找到某个bank,剩下的VA[2:0]用来从8字节中数据中找到某个字节,这种方式将两个连续的8字节数据放到两个相邻的不同的bank中,利用空间局部性原理,使得对这两个8字节数据访问落在不同的bank中。

由于Cache的每个端口在访问时候,都会同时访问两个way中的数据,然后根据Tag的比较结果来从两个way中选择命中的那个,所以Cache的一个端口在访问的时候,会同时访问到两个bank,每个way各一个。

支持虚拟存储器的处理器中,最常见的页面大小page为4KB,这需要VA[11:0]来寻找页面内部,因此对于48位的虚拟地址来说,剩下的VA[47:12]就作为VPN(Virtual Page Number)来寻址TLB,得到物理地址中PFN(Physical Frame Number)[39:12],它用来和Tag部分进行比较,判断是否命中。

对于一个2-way组相连的Cache来说,相比于单端口的实现方式,两个端口的实现方式所需要的控制逻辑电路基本上扩大了一倍,需要两个TLB、两个Tag比较器,还需要两倍Tag存储器,Opteron处理器采用将Tag SRAM复制一份来实现双端口的SRAM,当然也可以采用真实的双端口SRAM来实现这个功能,面积也不会减少多少,速度还会变慢。

除了Cache中存储数据的Data SRAM没有被复制之外,其他的电路基本上被复制一份,因此采用multi-banking方式来实现双端口的Cache,面积会增大很多,但是它的好处是速度比较快,对处理器的周期时间有比较小的负面影响。

2.4 超标量处理器的取指令

如果一个超标量处理器在每周期可以同时解码4条指令,这个处理器就称为4-way的超标量处理器,处理器每周期应该能够从I-Cache中至少取得4条指令,才能喂饱后续的流水线,如果少于这个值,则会造成后面的流水线浪费。

对于一个n-way超标量处理器来说,它给出一个取指令的地址后,I-Cache应该能够至少送出n条指令,称这n条指令为一组(fetch group)。I-Cahce支持这个功能最简单的方式就是使用data block的大小为n个字,每周期将其全部进行输出。如果处理器送出的取指令地址是n字对齐的,那么此时就可以实现每周期从I-Cache中读取n条指令的功能,在数据块data block部分需要n个32位的SRAM,当I-Cache命中时,这些SRAM会同时进行输出。但是存在跳转指令,处理器送出的取指令地址不可能总是n字对齐。

当取指令的地址不再是字对齐时,一组fetch group中的指令就可能落在两个Cache line中,但是对于Cache来说,每周期只能访问一个Cache line,这会导致在一个周期内无法取出4条指令使得后续流水线无法得到充足的指令,使部分资源空置。

处理器每周期取出的指令多于它能够解码的指令个数,通过一个缓存来将多余的指令缓存起来,这样就可以使后续的流水线得到充足的指令,避免了硬件资源的浪费,这些指令会存储到一个称为指令缓存Instruction Buffer,IB,后续的指令解码器会从指令缓存中取指令。

即使当前周期送出的取指令地址不是四字对齐,只要以后的周期中不出现引起指令的执行顺序改变的情况,例如分支指令和异常等情况,下个周期取指令的地址也会变成四字对齐,此时就可以在一个周期内取出四条指令了。

其实在现实世界中,分支指令出现的频率还是比较高的,这一方面会导致取指令的地址无法四字对齐,另一方面还会在分支指令执行的时候导致过多的无用指令进入流水线,因此需要对分支指令进行预测。

即使取指令的地址不是四字对齐的,仍旧可以在一个周期内读取4条指令,最简单的实现方法是使数据块data block大小变大,例如其包含8个字,只要取指令的地址不是落在数据块的最后三个字,就可以在每周期内读取4条指令。这样做的前提是增大了数据块的容量,如果在Cache的总容量一定的情况下,意味着Cache set的个数会减少,可能会增大Cache miss的概率

而且,如果Cache的每个数据块是8个字,是不是也需要使用8个32位的SRAM来实现呢? 问题就是如果SRAM的个数过多,会导致保护电路也占用过多的面积,而且要从8个SRAM的输出中选用4个字也是一件很浪费电路的事情。

若一个Cache line中包含的8个字实际上占据了SRAM的两行,因此共使用了4个32位的SRAM。其中需要一段重排序的逻辑电路,对4个SRAM的4条指令进行重排序,使它们满足最原始的指令顺序,这样才能够使后续的流水线得到正确的指令。此外,当取指令的地址指向了Cache line中最后的三条指令的某一条时,此时本周期并不能输出4条指令,因此在重排序逻辑电路中还需要假如指示每条指令是否有效的标志信号,这样才能够将有效的指令写入到后续的指令缓存Instruction Buffer中。

为什么要实现分支预测呢?因为喂满流水线所需的并行度很高,一条指令从进入流水线直到结果被计算出来,中间会经历很多段流水线,如果使用最简单的静态预测方法,那么一旦发现这条分支指令是需要执行的,就要将流水线中,该分支指令之后进入流水线的所有指令都抹掉,也就是这段时间做了无用功。如果能够提前知道在本周期取出的指令中,那条指令是分支指令,且可以预知这条分支指令的结果,那么就可以减少流水线被打断的次数。

实现分支预测之后,从I-Cache中取指令的同时,已经可以知道当前指令组fetch group中那条指令是不是分支指令,如果它被预测执行,那么指令组中位于它之后的指令就不应该进入到后续的流水线。

在实现虚拟存储器VA的处理器中,处理器送出的取指令地址是虚拟地址,需要使用一定的方法将其转换为物理地址,然后才能够从存储器中取指令,在这个转换过程中可能发生很多事情,它们都可以打断流水线的正常执行。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
超标处理器设计》讲述超标(SuperScalar)处理器设计,现代的高性能处理器都采用了超标结构,大至服务器和高性能PC的处理器,小至平板电脑和智能手机的处理器,无一例外。《超标处理器设计》以超标处理器的流水线为主线展开内容介绍。《超标处理器设计》主要内容包括超标处理器的背景知识、流水线、顺序执行和乱序执行两种方式的特点;Cache的一般性原理、提高Cache性能的方法以及超标处理器中的Cache,尤其是多端口的Cache;虚拟存储器的基础知识、页表、TLB和Cache加入流水线后的工作流程;分支预测的一般性原理、在超标处理器中使用分支预测时遇到的问题和解决方法以及如何在分支预测失败时对处理器的状态进行恢复;一般的RISC指令集体系的简单介绍;指令解码的过程,尤其是超标处理器中的指令解码;寄存器重命名的一般性原理、重命名的方式、超标处理器中使用寄存器重命名时遇到的问题和解决方法以及如何对寄存器重命名的过程实现状态恢复;指令的分发(Dispatch)和发射(Issue)、发射过程中的流水线、选择电路和唤醒电路的实现过程;处理器中使用的基本运算单元、旁路网络、Cluster结构以及如何对Load/Store指令的执行过程进行加速;重排序缓存(ROB)、处理器状态的管理以及超标处理器中对异常的处理过程;经典的Alpha21264处理器的介绍。在本书中使用了一些现实世界的超标处理器作为例子,以便于读者加深对超标处理器的理解和认识。 《超标处理器设计》可用作高等院校电子及计算机专业研究生和高年级本科生教材,也可供自学者阅读。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值