高速缓存技术

高速缓存

5e2166ac-9ae9-4019-8865-0eaa179c89cc

在计算机发展的过程中,一直存在一个有挑战性的问题:如何设计与CPU速度匹配的内存系统。尤其是过去十几年来,内存系统和CPU的“相对速度”是越来越慢的。现代CPU对内存的延迟带宽有高的要求,但是内存系统在这两方面通常是矛盾的,如果增大带宽会导致延迟的升高。比如说可以通过流水线技术来让多个内存访问“重叠”,从而增大总的带宽,但这会让每个单次内存访问的延迟更大。

解决内存慢的主流方案是在内存系统中使用高速缓存技术(cache),其中分离式高速缓存是种能有效解决带宽和延迟的矛盾的方案,其分别为数据指令提供缓存,并允许在这2种缓存并行的进行访问。并行能让带宽翻倍,而这种带宽翻倍并不增加延迟。比如MIC-1就提供了用于指令和数据的两种内存访问端口,如果让这两个端口各有自己的缓存模块,那这两个缓存模块可独立的访问内存。

上图是种更多层次的缓存方案,其第1级缓存是位于CPU芯片的分离式高速缓存,有几十KB。第2级缓存不区分数据和指令,其不在CPU芯片上,但和CPU芯片封装在一起,有数百KB。第3级缓存同样不区分数据和指令,其在CPU外的主板上,有几MB。这个结构事实上是早期INTEL处理器的缓存架构。

缓存通常会基于2种地址的局部性提高其命中率:

1) 时间局部性:最近被访问的地址,将来被访问的可能性更大;

2) 空间局部性:最近被访问的地址附近范围的地址,将来被访问的可能性更大;

这两种局部性在使用到栈时,体现的尤其明显。当基于时间局部性缓存的数据没有被命中时,很多缓存算法都是把最近最不常用的项替换为新数据。

所有缓存系统的设计都遵循如下的模型。首先是把内存按块/行(Line)进行“编址”,每个块的大小为4~64个连续字节,块从0开始编号。任何时刻缓存中都会有不同的块,当需要读取内存上的字时,缓存控制器会先检查该字是否位于当前缓存的某个块中,如果在的话就能减少一次内存读取,如果不在,从更低级的缓存或内存中取出相应的块,并用新的块替换掉某些旧的块。至于被替换的块是哪些,其基本思想是让缓存尽可能存储最常用的块,尽可能提高缓存命中率。

 

直接映射高速缓存

f8520755-0b0f-4ca6-ab11-e7e9f4e4b8a0

直接映射高速缓存是最简单的1种方案,上图(a)是种具体例子,其只含1级缓存,共有2048个项,每个项保存1个块,每个块32字节,所以其共存储64KB的内存数据。项由如下3部分组成:

1) 有效位(Valid):表示该项是否有效,初始化时所有项都标为无效;

2) 标记字段(Tag):缓存块对应的内存块地址,所有Tag都是唯一的16位值;

3) 数据字段(Data):存储内存内容的拷贝,有32字节;

另外这2048个项都有自己0-2047的索引,这些索引可以用11位二进制表示。对于内存块来说,每个内存块都映射到固定的索引上,这个映射规则可以是这样,内存第0-2047个块使用第0-2047索引的缓存项,第2048-4095个块使用第0-2047索引的缓存项,内存第4096-6143个块使用第0-2047索引的缓存项……这就是直接映射高速缓存的核心思想。对于任意一个内存块,其能存入的项是唯一的也是确定的。并且在整个缓存中,大部分数据都大概率是连续的内存块

对于包含上述缓存的内存系统,内存地址可以编成上图(b)那样的格式,其由4个部分组成:

1) TAG:“按块编址”后的内存块地址,16位;

2) LINE:缓存的项编号,11位;

3) WORD:指明块中第k个字。通常块中包含多个字;

4) BYTE:指明块中第k个字节。用于按字节取数据。缓存不支持按字节取数据时,该项恒为0;

CPU给出这种内存地址并发起read后,首先会根据LINE去找到缓存项,如果这个项编号有效,那么就继续对比项的Tag和内存地址的TAG,两者相等时称为缓存命中,两者不相等或VAILD位为无效则称为缓存缺失。当发生了缓存缺失,那么下一步就会从内存中取出对应的块,并用于替换掉当前的项(如果当前项之前被CPU写过且没更新回内存,要先将其写回内存再替换掉)。CPU通常不会串行的去先判断缓存是否有效然后再取出对应的字/字节,而是会并行做“判断是否有效”和“取对应字/字节”。

这种方案有其缺点,比如假设第X个内存块和第X+2048个内存块是最经常被访问的2个块,那么好的缓存方案应该让这2个块都在缓存中。但是由于这两个内存块的LINE号相同,所以它们对应的缓存项会被反复覆盖。如果CPU一直交替的访问这两个块,那么相应的缓存项一次都不会被命中,在这种极端情况下,有了缓存反而更慢了。不过这种情况通常可以避免,比如优秀的编译器就能合理安排地址,从而避免这种冲突。另外还可以采用分离式缓存避免这种冲突,因为2048个内存块(64KB)是一个很大的跨度,这么大的跨度很可能因为指令地址数据地址跨度大导致的,指令和数据本身出现这种跨度的情况相对少。

 

组相联高速缓存

f43a94e5-4da1-4633-a528-dba5cf255439

前面提到了直接映射高速缓存的重要缺点,和避免该缺点的方法。这里从解决缺点的角度出发,引入1种能有效减少冲突的组相联高速缓存方案。这种方案的思想本身很简单,就是让多个项“共享”原来的项索引,从而实现一个项缓存多个块,进而减少冲突。上图为一个4路的组相联高速缓存。

在访问上述4路组相联缓存时,在定位到LINE之后,需要比较4次TAG来确定内存块是否在缓存中,并且整个缓存的面积也增大了4倍。但这样是值得的。另外需要注意到,对于组相联缓存,其除了继承了直接映射高速缓存隐含的缓存替换策略外,还需要进一步确定“新的项进来时,把这4个项中的哪个替换掉”。一种目前常用的算法是最近最少使用算法(LRU)。其会让最少被用到的那个被替换掉。

对于组相联高速缓存,太多的“子项”不一定带来收益,因为LRU是用表把每项的子项按访问次序排列的,如果子项太多,重新排列很慢,所以最常用的就是上图中的4路组相联高速缓存

 

对写操作的处理

当CPU对内存的某个字进行写入,并且该字也位于缓存中时,那么只有这2种处理:

1) 把内存和缓存都进行更新;

2) 只更新内存,把对应缓存标记为无效;

几乎所有计算机都选择了(1),在这个基础上又有2种 策略:

1) 写直达:立即更新缓存和内存;

2) 拖后写:立即更新缓存,但不立即更新内存,直到相应的块需要被替换时才更新内存;

两种方案前者更简单,更可靠(比如缓存出错要从内存恢复数据,就比后者更可靠),但是写直达可能会导致更多次的内存写操作(比如对同一个块修改了两次,拖后写只需要写一次内存)。

还有一个问题是CPU对内存某个字进行写入,但该字不在缓存中(这种情况称为写缺失),那么该如何处理?是否需要把写缺失的块存入缓存中?大多数计算机是这样处理的:

1) 采用写直达的计算机通常不会把写缺失的块调入缓存,而是直接写入内存;

2) 采用拖后写的计算机通常把写缺失的块调入缓存,然后继续拖后写,该方案也叫写分配

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部