二、Cache
-
为什么需要Cache
在计算机中存在时间相关性和空间相关性,而存储器的访问速度远跟不上处理器的速度,因此需要Cache存储访问概率大的数据。- 时间相关性:如果一个数据现在被访问了,那么在以后很有可能还会被访问;
- 空间相关性:如果一个数据现在被访问了,那么其周围的数据以后有可能也会被访问。
-
Cache的结构
Cache主要由两部分组成,Tag部分和Data部分。
Data部分保存一片连续地址的数据,tag部分存储着这片连续数据的公共地址。
一个Tag和它对应的所有数据组成的一行称为一个Cache line,而Cache line中的数据部分称为数据块(Cache data block, 也称为Cache block)。
如果一个数据可以存储在Cache中的多个地方,这些被同一个地址找到的多个Cache Line称为Cache Set。
-
3C定理
Cache只保存最近被处理过的内容,由于容量有限,很多情况下要找的数据和指令不在Cache中,称为Cache缺失,影响Cache缺失的情况:- Compulsory,由于Cache只是缓存以前访问过的内容,因此,第一次被访问的指令或者数据不在Cache中;
- Capcity, Cache容量越大,就可以缓存更多的内容,因此容量是影响Cache缺失发生频率的一个关键因素;
- Conflict, 为了解决多个数据映射到Cache中同一个位置的情况,一般使用组相连结构的Cache。
-
Cache的组成方式
-
直接映射
1> 对于物理内存中的一个数据,如果在Cache中只有一个地方可以容纳它,就是直接映射。
2> 直接映射中cache line一般有三个组成部分,分别是有效位V,标志位Tag,和数据位Data block。
3> CPU送来的地址按高低位被分成三部分,tag、index和offset。index用来指定选中哪一个cache line,tag用来与cache line的tag作比较以生成hit信号,而offset则从选择的cache line中选中部分数据进行输出。
4> 实现结构最简单,执行效率最低。
-
组相连
1> 对于物理内存中的一个数据,如果在Cache中有多个地方可以容纳它,就是组相连。
2> 同样使用Index对Cache寻址,对于找到的多个Cache line,通过比较其Tag确定结果,如果Tag都不匹配,则Cache缺失。
3> 实际实现中,Tag和Data分开放置,称为Tag SRAM和Data SRAM。同时访问这两个则称为并行访问,先访问Tag再找Data则称出串行访问。
-
全相连
1> 对于物理内存中的一个数据,如果在Cache中任意地方可以容纳它,就是全相连。
2> 不再使用Index,而是直接使用Tag比较得到Cache line,相当于直接使用存储器的内容来寻址,即内容寻址的存储器(Content Address Memory,CAM)。
3> 实际实现中使用CAM来存储Tag值,使用普通的SRAM来存储数据,CAM中寻址成功时对应的SRAM对应的行也将会被找到。
4> 灵活度最大,缺失率最低,但是比较最多,延迟最大。一般不会有很大的容量,比如TLB使用这种结构实现。
-
-
Cache的写入
- L1 Cache分为I-Cache和D-Cache,对于I-Cache,一般不直接写入内容,而是借助D-Cache实现,将要改写的指令作为数据写入D-Cache,再将D-Cache的内容写入下级存储器如L2 Cache,并将I-Cache的所有内容置为无效,这样处理器再次执行时会使用到修改的指令。
- 对于D-Cache,执行一条store指令时,假设要写入的地址在D-Cache中,如果只写入D-Cache而被不写入下级存储器,那二者中对于同一个地址就有不同的数据,称为不一致。关乎两种情况:
- 写通(Write Through):要保持一致性,最简单的就是数据写入D-Cache时也写入下级存储器。问题在于:下级存储器的访问时间长,而store指令在程序中出现频率高,频繁向慢速存储器中写入数据,则执行效率低。
- 写回(Write Back):如果数据写到D-Cache后,只是将被写入的Cache-line做一个标记,只有当被标记的line要被替换时才写入下级存储器。被标记的记号称为脏(dirty)状态,这样可以减少向慢存储器写入的频率,但是会造成D-Cache和下级存储器的不一致,为存储器的一致性管理增加负担。
- 如果要写入的地址不在D-Cache中,就发生了写缺失。有两种策略:
- 写不分配(Non-Write Allocate):最简单的处理方法就是数据直接写到下级存储器,而不写入D-Cache.
- 写分配(Write Allocate):先把下级存储器中要写入地址的data block取出并于要写入D-Cache的数据合并,再将这个data block写入D-Cache中(写通/写回的方式)。
- 一般情况下,“写通”和“写不分配”组合,直接将数据更新到下级存储器;“写回”和“写分配”组合。“写通”“写不分配”组合的工作流程、“写回”“写分配”组合的工作流程为:
-
Cache的替换策略
读取或写入D-Cache缺失时,都需要从对应的Cache Set中找到一个line存放从下级存储器读取的数据。如果Cache Set的line都被占用了,就需要替换。常用替换策略有:- 近期最少使用
LRU(Least Recently Used, LRU)的基本思想是选择最近一段时间使用次数最少的Cache line进行替换,需要对一个Cache set中的每一个cache line的使用情况进行跟踪,实现方法可以是为每一个Cache line都设置一个“年龄位”。当way的数量较多时,为每个way设置年龄位变得复杂,使用“伪LRU”对way进行分组设置(类似编码)实现。
- 随机替换
随机替换不需要记录年龄,只需要一个内置的时钟计数器,每当要替换cache line,就根据计数器的当前计数结果来替换cache line。优点是实现起来很简单,缺点是不能体现出数据的使用的规律,可能把最近最新使用的数据给替换出去,不过随着cache的容量越来越大,这个缺点所带来的性能损失也越来越小。总的来说,这是一个折中的办法。
- 近期最少使用
-
Cache性能的提高
- 写缓存
当D-Cache发生缺失,就需要从下级存储器读写数据,一般访问缓慢甚至只有一个读写端口。比如将缺失的数据从下级存储器中读出并写入选定的Cache line中,如果这个line是脏状态的,那就需要先将这个line写入下级存储器,才能将缺失的数据从下级存储器读出写入Cache中,这是一个串行的过程。
使用写缓存就可以将这个line的数据写到缓存中,直接将缺失的数据从下级存储器中读出然后写入这个line中,缓存中的数据再伺机写入下级存储器。这样D-Cache写入下级存储器的时间被屏蔽了。
缺点在于写缓存中的数据是最新的,读D-Cache发生缺失时,需要再写缓存中查找数据,增加了设计复杂度。 - 流水线
读取D-Cache时,可以同时读取Tag SRAM和Data SRAM,基本可以在一个周期完成读取操作;
写入D-Cache时,必须先读取Tag SRAM,确认写地址在Cache中才能写Data SRAM,串行实现无法在一个周期完成。采取流水线结构,典型方式是将Tag SRAM的读取和比较放在一个周期,写Data SRAM放在下一个周期。 - 多级结构
存储器的容量于速度相互制约,使用多级Cache结构进行提升。
一般L1 Cache的容量小,保持与处理器内核速度相同等级,L2 Cache容量大但是速度更慢。高阶处理器还会使用容量更大的L3 Cache。
一般L1 Cache可以采用写通方式,简化流水线设计,而L2 Cache采用写回方式。二者之间的包含关系为:- Inclusive:L2 Cache包含L1 Cache,比较浪费硬件资源,简化了一致性管理,多数处理器使用;
- Exclusive:L2 Cache包含L1 Cache,避免了硬件资源浪费,增加了一致性管理负担。
- Victim Cache
Cache中被“踢出”的数据可能马上又要被使用,使用Victim Cache(VC)来保存最近被踢出的数据。一般采用全相连的方式,容量比较小(一般4~16个数据)。
本质相当于增加了Cache中way的个数,能够避免多个数据竞争Cache中有限的位置,降低缺失率。
类似的设计是Filter Cache,设置在Cache之前,当一个数据第一次被使用是首先放在Filter Cache中,再次被使用时才会搬移到Cache中。作用时防止偶然使用的数据占据Cache,对偶然使用的数据进行一个过滤。
- 预取
本质是一种预取技术,猜测处理器在以后可能使用哪些指令或数据,提前将其放到Cache中,用以缓解Compulsory造成的缺失问题。- 硬件预取
对指令而言预测相对简单,因为程序是串行执行的,在没有分支指令的时候只要在访问I-Cache时将后一个数据块也取出即可。
当I-Cache发生缺失,从下级存储器取出数据放去I-Cache时,将下一个数据块也取出放在Stream Buffer中。
后续执行中在I-Cache中发生缺失而在Stream Buffer命中时,将当前数据写入I-Cache且将下一个数据块加入到Stream Buffer中。无分支指令时效率高,分支指令会使之造成浪费。
- 软件预取
硬件实现数据预取不理想,采用软件预取的方式。在程序编译阶段对程序进行分析,得知需要预取的数据,如果指令集设有预取指令,编译器就可以控制程序进行预取。
预取时间是关键:预取时间太晚,执行时还没预取出来则无意义;预取时间太早, 有可能踢出D-Cache中一些原本有用的数据,造成Cache的“污染”。
- 硬件预取
- 写缓存
-
多端口Cache
超标量处理器中需要每周期同时执行多条load/store指令,这需要一个多端口的D-Cache。但D-Cache容量大,采用物理的多端口设计会对芯片的面积和速度产生大的影响。因此需要其他方法实现多端口的D-Cache。- True Multi-port
直接使用一个多端口的SRAM实现多端口的D-Cache,缺点在于Cache的所有控制通路和数据通路都需要复制,即多套地址解码器、多个多路选择器、多个比较器、多个对齐器等,会增大面积。而且多端口的SRAM Cell需要驱动多个读端口,延迟、功耗都会增大。 - Multiple Cache Copies
将Tag SRAM和Data SRAM进行复制,本质也是True Multi-port,只不过是通过复制Cache而使得SRAM不需要使用多端口结构。但要保持两个Cache的同步,同样复杂。 - Multi-banking
实际广泛使用的方法。将Cache分为多个bank,每个bank都有一个端口(单端口的SRAM)。
同样需要多套地址解码器、多个多路选择器、多个比较器和多个对齐器,而Data SRAM不需要实现多端口的结构。由于需要判断Cache每个端口的命中情况,所以Tag SRAM需要有多端口读取功能,需要采用多端口结构。
一个周期访问地址在不同bank则可以实现并行处理,如果在同一个bank,则会产生bank冲突,这是影响多端口Cache性能的关键因素。
- True Multi-port
参考资料:
《超标量处理器设计》——姚永斌