第二章 Cache
在超标量处理器中,分支预测和cache直接影响这处理器的性能。分支预测的精度,cache的命中率和访问速度。
2.1 Cache的一般设计
1、cache的简单介绍
近代处理器的运行频率越来愈高,但是存储器发展没这么快,需要在处理器内核和存储之间有这种类似过渡的部件,其中存放的是最近被访问过的数据,或者最近被访问过的数据附近的数据,这就是所谓的时间相关性和空间相关性。
L1cache和L2cache是使用SRAM实现,物理内存是使用DRAM实现,再然后是硬盘/闪存。
其中L1cache在当今的哈佛结构中,分为I-cache和D-cache,分别存储指令和数据,L1cache因为紧挨着处理器内核,所以需要有很快的速度和内核频率匹配,所以容量就不能很大,当L1cache出现缺失时,需要向L2cache访存数据,因此L2cache的容量就要大,以求全,可以容忍速度慢一点。
在多核处理器的环境中,L2或者L3cache会作为共享cache,多核数据共享, 但至少L1cache肯定是每个核私有的。
D-cache需要满足访存指令的操作,对于超标量来说就会在同一周期内执行多条指令,就需要设计多端口,但对于D-cache容量本身就比较大,所以再加入多端口,就会导致该部件占用着较多的面积和较高的演示;相较于其他多端口的,如发射队列(IQ)、sbuffer、register file等,这些容量较小,多端口的影响就不大。
由于L2-cache是在L1缺失的时候才去访问,所以L2访问概率较低,就不需要多端口的设计。L1需要多端口。
2、cache的组成结构
cache是由tag和data两部分组成,data部分鉴于数据访问的时间相关性和空间相关性,所以存储的是一片连续地址的数据,该片连续地址的公共地址部分就是tag;某一tag和该tag对应的一大片数据称为一个cacheline。
cacheline的实现方式有三种,直接映射、组相联、全相联,直接映射和全相联是组相联的两个极端方向,后续章节对三者分别做解释。
组相联 使用场景I-cache、D-cache
全相联 使用场景 TLB、victim cache
- cache的缺失原因
cache的确实会直接影响到处理器数据访存的性能,缺失一般有三大原因,被称为3C定理。
(1)强制缺失(compulsory):cache缓存的是以前访问过的内容,如果某数据是第一次被访问到,就会导致强制缺失,需要采用预取的方法降低强制缺失发生的频率,有软件预取和硬件预取两种方式。
(2)容量缺失(capcity):举例,某cache有4个cache set,如果5个数据频繁被访问,但分属于不同的cache set,就会导致容量缺失。(访问数据超出set的个数)
(3)冲突缺失(conflict):举例,某2-way cache,表示一个cache set是两个cacheline,但是如果有3个同属于一个set的数据频繁访问,就会冲突缺失,需要采用victim cache的方法解决。(访问数据超出一个set中的cacheline的个数)、
无法从根本上解决,只能是降低cache缺失的发生频率。
2.1.1 cache的组成方式
- 直接映射
是最基本,最简单的一种映射方式,访问cache的地址由三部分组成,分别是tag+index+offset,index是用来索引cacheline的,tag用作对比,offset是cacheline中data数据块的偏移选择。
同时,在cacheline中还会有个vaild位,标志是否保存着有效数据。
对于所有index相同的存储器地址,都会寻址到同一个cacheline,如果,两个index相同的存储器地址交互的访问cache,就会导致cache缺失(强制缺失?)。
通过index找到cache line之后,只代表访问的地址对应的数据可能存在这个cache line中,但是也有可能是其他地址对应的数据。所以,又引入tag 区域,tag 和data 一 一对应。每一个cache line都对应唯一 一个tag,tag中保存的是整个地址位宽去除index和offset使用的bit剩余部分。
一个典型的直接映射的占用空间:
32位存储器地址,offset5位,可以寻址数据块(cacheline的数据部分)内32Byte的数据,index6位,可以寻址64个cacheline,剩余地址位21位,作为tag位。
该cache可以存储的数据为64*32Byte=2048Byte,即2KB;tag位大小为64*21bit=1344bit≈1.3Kb;有效位大小为64*1bit=64bit。一般用数据部分的大小作为该cache的大小,所以该cache大小为2KB,但是额外占用了1.3Kb(tag+vaild)左右的空间。
总结,最简单,不需要替换算法,执行效率低。
- 组相联
一个n-way的cache,代表通过一个index可以索引出n个cacheline,这n个cacheline称为一个cache set,或者说该cache中一个set有n个cacheline;那么tag就可以用来比较是哪个cacheline,如果tag都不相等,就说明是cache缺失。(属于冲突缺失?)
这种相较于直接映射可以降低cache缺失的概率,因为一个数据可以放在cache set的n个地方。
在实际设计中,会把cache的tag和data分开放置,分别存放在tag SRAM和data SRAM中,对应的访问方式有:
- 并行访问,通过index分别索引tag SRAM和data SRAM,读出的tag对比地址中的tag,在读出的数据块中选择匹配的一个,再offst访问字节数据;
- 二串行访问,先通过index索引tag SRAM,读出的tag对比地址中的tag,通过该匹配的tag,只访问指定的那个SRAM,这时候index可以索引出匹配的数据块,进而offset访问字节数据。
因为data SRAM不需要都访问,只需要读取匹配的,所以功耗降低,而因为串行执行,缺点就是延时较大,虽然执行周期多了一个,但是对于位于相关性顶端的load指令来说,就会影响到访存指令的执行效率。
总结来说,并行访问会有较低的时钟频率和较大的功耗,但是优点是,访问过程的减少一个时钟周期,比较适合顺序超标量的处理器;串行访问因为增加了一个时钟周期,有较高的时钟频率,并不会因为这个增加的周期导致性能明显降低,适用于乱序超标量的处理器。
cache的访问时间比较长,属于处理器中的关键路径。
- 全相联
如果一个数据可以放在任意一个cacheline中,就属于全相联的结构。因为可以放在任意位置,就不用index索引了,在访问地址中,只包含tag+offset部分。
那通过什么索引呢,就需要依靠内容索引了,通过直接对比cacheline中的内容,即tag部分,跟访问地址的tag做对比,这种叫内容寻址的存储器(content address memory,CAM)。
将tag存储到CAM中,将数据存储在SRAM中,在匹配到tag对应行时,将SRAM对应行数据也读出,offset到对应的字节数据。
正因为全相联结构,数据可以存放在任意位置,所以缺失率最低,但是由于需要对每一项cacheline都要做内容对比,所以延时最大,所以注定不能拥有太大的容量,目前主要应用在TLB和victim cache中。
2.1.2 cache的写入
- 写通和写回
在数据写入过程中,往往牵扯到上下级cache之间的数据更改问题,为了保持两者cache数据的一致性问题,有两种操作方式:
- 写通(write through):在数据写到D-cache的同时,也写到下级cache或者存储器中,但是往往L2 cache的访问速度比较慢,会影响store指令的执行速度。
- 写回(write back):在数据写到D-cache之后,暂时不写到L2 cache,而是在该cacheline做一个记号,称为dirty,只有该cacheline要被替换掉时,才将其写到下一级cache。这种操作可以减少频繁访问L2 cache的次数。但是会因为该种方式造成两级cache之间很多数据不一致,不利于缓存一致性的管理。
- 写分配和写不分配
如果在写D-cache时,发现写地址不在其中(cache miss),可以直接将数据写到下级存储器,不写D-cache,称为Non-write allocate,反之称为write allocate。
- non-write allocate:在D-chche中发生cache miss时,直接将数据写到下级存储器(不需要将其数据块取出合并之类的操作),不经过D-cache,这种直接写到下一级属于写通(write through)。load指令访问缺失时,还是要从下级取出匹配的,放到该cache中的一个cacheline中的,然后从D-cache中读数据。
- write allocate:cache miss时,在下级存储器中将对应地址的cacheline数据块取出,将写入的信息合并其中,并将其写到D-cache,即分配给D-cache。该line同时标记为dirty状态,在发生替换时,才将该cacheline更新到下级存储器,即为写回(write back)。
这种访存操作比较复杂,首先不管是读取还是写入,都需要在D-cache中找到一个“冤大头”替换掉,该被替换的,如果是dirty状态就先写回下一级,更新下一级存储的数据,保持一致性,如果不是dirty状态,说明上下两级的数据一致,直接舍弃掉就行。然后从下一级cache中找到正确的cacheline之后放在被替换掉的位置。
这个操作过程中如果被替换掉的是dirty,需要对下级存储器两次访问,第一次写回更新数据,第二次取出正确的cacheline。如果是写cache,写入之后还得dirty标记。
虽然该种情况复杂但是,只有被替换时,才会访问下一级存储,所以对于慢速的下一级访问次数变少,速度自然就上去了。
2.1.3 cache的替换策略
在前文提的这两种写入或者读出的方式,除了写通&不分配这种方式中的store指令操作在缺失时,不需要从当前cache找一个line来存放下级数据,而是直接写到下级存储器。在这个过程中,将一个line替换掉,牵扯到一个替换策略。这就是所谓的冲突缺失导致的替换策略。
- 近期最少使用法(LRU)
在每一个cacheline设置年龄位,2-way只需要一位年龄位,当访问时,就给其置1,其他置0,替换掉0的line,(同理,n-way的cache,每一个cacheline需要的年龄位的位数为log2(n) )。这种方法的局限性是,随着way的数量的增加,这种不再合适。
- 伪LRU
将所有的way进行分组,每组使用1位的年龄位,n-way总计才需要log2(n)位年龄位。
3、随机替换
Cache 的替换算法一般都是使用很复杂的硬件来实现的,影响处理器的周期时间,于是就有了随机替换(Random Replacement)的实现方法。随机地选择一个 way 进行替换,这种方法发生缺失的频率会更高一些,但是随着 Cache 容量的增大,这个差距是越来越小的。
一般采用一种称为时钟算法(clock algorithm)的方法来实现近似的随机,它的工作原理本质上就是一个计数器,例如一个八路组相连结构的 Cache(8-way set-associative),则计数器的宽度需要三位,使用计数器当前的值,从被选定的 Cache set 中找到要替换的 line,近似地实现了随机的替换,可能并不能获得最优化的结果,但是它的硬件复杂度比较低,也不会损失过多的性能。
这种时钟算法我暂且称之为玄学算法。
2.2 提高cache的性能
2.2.1 写缓存
对于写回操作时,如果发生cache缺失,load和store指令都需要将替换的line(如果是dirty状态),需要将其写到下级存储器更新数据,但是访问下级存储器的时间太久,影响效率,所以将其先写入一个write buffer中,再择机写回下级存储器。
对于写通的操作,也是在写到D-cache的同时,写到write buffer中,写通操作便于进行存储一致性的管理,多核处理器中常见该种设计。
加入写缓存之后,在load数据时,如果D-cache发生缺失,下级存储器和write buffer都需要访问,下级存储器就通过地址访问即可,但是write buffer需要加入地址比较的CAM电路。因为在其中数据往往是比较新的,所以如果同时在两个地方找到数据,使用write buffer中的新数据。
由于写通操作对于下级存储器的访问频率较高,所以在写通操作中,写缓存很重要。
2.2.2 流水线
读取D-cache时,tag SRAM和data SRAM可以同时读取,处理器周期时间要求不是很严格时,可以在一个周期内完成。
写D-cache是,读取tag SRAM和写入data SRAM的操作串行完成,先tag读出比较,再写对应的data SRAM。
在load数据时,数据可能在data SRAM也可能在store流水线中,所以需要在load指令时对比store指令流水线中的地址,如果匹配,直接读取store的data,这种叫前递数据。
2.2.3多级结构
多级cache,在现代多核处理器中,L1和L2 cache为其单核私有的cache,L3 cache作为多核共享的cache。
在L1中可以使用写通的设计方式,可以简化流水线,在L2中会使用写回的设计方式,对于写通的来说,便于在多核环境下,保持存储一致性。
2.2.4 victim cache & filter cache
在组相联结构的cache中,比如一个2-way组相联,如果有三个数据同属于该set,但是频繁换入换出,这是一种频繁的冲突缺失,不利于cache的访存性能,如果在cache“之后”,加入一个victim cache,将最近刚被踢出的line保存在victim cache中,相当于增加了way的个数,避免多个数据竞争有限的way,由于容量比较小,通常采用的全相联的结构,cache和victim cache之间属于互斥关系(exclusive)、
在访存数据的时候,处理器同时读取这两种cache,如果在victim cache中发现了我们想要的数据,将其换入cache中,两者交换数据,效果等同于cache命中(就相当于cache额外增加的way)。
另外一种filter cache,是使用在cache“之前”,在访存cache时还有一种强制缺失,表示该数据第一次访存到,将数据先放入到filter cache中,防止该数据仅仅访存一次后边就不用了,如果过早的搬入cache中,会占用有限的way,就先放在filter中,如果数据再次被访问到,才会将其搬入cache中。
2.2.5 预取
强制缺失,某个数据第一次被访问到时必然是缺失的。
可以通过预取解决,分为硬件预取和软件预取。
1.硬件预取
- 指令预取(I-cache)
访问Icache中一个数据块,将后边的数据块也取出来放到Icache中。但是分支指令导致后边的指令数据块不会被使用,这些不会被使用的数据块占用了cache的空间,所以造成了“cache 污染”。
解决方式就是将下一个相邻的数据块放在stream buffer中,当用到这个数据块时就将其放到Icache中,并将再下一个相邻的放在stream buffer中。但是如果分支预测跳转,那么取出来的这个相邻数据块浪费了总线带宽和功耗。
- 数据预取(D-cache)
一般情况下的操作,访问D-cache中一个数据块,将后边的数据块也取出来放到D-cache中。但是这个情况不像Icache,指令顺序读取有分支跳转才跨数据块,,对于D-cache来说,要访问的数据不一定就在下一个数据块中,这种预取就会浪费总线和位宽。
可以通过硬件观测访问数据块的规律,来进行预取,比如,访问的第一个数据是地址是a,访问的第二个是a+128,第三个是a+256,硬件就会根据这个规律将地址每+128的数据块预取出来。
2.软件预取
在程序编译阶段,编译器就对程序进行分析,,在其中如果有软件预取的相关程序,就会控制该程序进行预取。
软件预取的缺点,如果预取时间太晚,可能会导致使用时,还没有取出来;如果预取时间太早,可能会将原来cache中有用的数据被替换掉,占据cache空间,造成cache“污染”。
2.3 多端口cache
超标量处理器中,会面临多条指令同时执行的问题,所以会有一些多端口的部件,比如寄存器堆(register file)、发射队列(issue queue)、重排序缓存(ROB)等,这些部件的本身容量不大,多端口设计对芯片面积和速度的负面影响较小,但多端口的D-cache本身容量就很大了,多端口设计会使芯片面积和速度的负面影响较大。实现多端口D-cache有三种方式,true multi-port、multiple cache copies、multi-banking。
2.4 超标量处理器的取指令
n-way的超标量处理器每周期至少要从I-cache中取出n条指令,也就是n个字,最简单的方法就是使cacheline中数据块的大小为n个字的大小,直接在一个周期输出一个cacheline的数据,通过n个32位的SRAM将其同时输出。
但在这里如果出现了跳转指令,那就无法n字对齐,对于n-way的处理器,对应的cacheline也是n个字,那跳转指令出现就会导致一个组的指令跨两个cacheline,但是cache每个时钟周期只能访存一个cacheline,比如某4-way处理器在一个时钟周期只取了后三条指令,后续流水线无法得到充足的指令,导致后续资源的浪费,每周期可能取出的指令个数是1/4*4+1/4*3+1/4*2+1/4*1=2.5(这是极限情况,其实取满4条的情况占主),显然这种情况不能满足4-way处理器的指令执行需求,所以需要扩大cacheline的大小,使每周期从cacheline中取出的指令大于4条,将多余的指令放在IBuffer中备用,待发射。
其实只有在当前周期,才会因为跳转指令跳转到该处导致取不满4条,在下一周期就会恢复4条指令全部读取,,即只要不出现分支指令和异常情况等引起指令执行顺序改变的情况,都能正常取出4条指令。
其实通过增大cacheline中数据块的大小,可以极大的降低每次取指令因为跳转而无法对齐导致指令数不够的情况发生概率。例如数据块增大到8个字,对于4-way的处理器,只要取指令的起始地址不在最后3个字上,就可以实现每周期取4条指令。
那通过增大数据块大小,导致多取出来的指令就放在buffer中缓存,后续再择机发射执行。