高速缓存(cache)存储器

高速缓存(cache)存储器:

这里先说明一下(高速缓存)cache和(高速缓冲)buffer的区别:
buffer主要作用是在一定程度上减少对IO设备访问的次数,可以起到流量整形的作用,也提升了系统的性能,毕竟IO操作和内存和cpu的处理速度差距还是很大的,比如下载一个文件,是积攒到一定量才写回磁盘,而不是下载一个字节写回一个字节,这样过不了多久,磁盘就损坏了。
cache主要作用是缓解处理数据的两端速度不匹配这种情况带来的时间上的浪费,这样做就加快了访问速度。比如大家都知道的cpu和内存之间的速度差异就特别大,所以需要cache缓存数据,以便提高速度。
总之一句话,buffer主要解决了空间上的问题,而cache主要解决了时间上的问题

众所周知,寄存器和内存之间读写速度的差距是很大的,而内存和磁盘之间的读写速度差距也很大。当内存需要传数据给寄存器,假如有这样一个读写速度处于寄存器和内存之间的东西,可以帮忙存储内存要传的数据,然后再把这些数据传给寄存器,这样不就提高了速度了吗。高速缓存存储器就是这样一个东西,在两端读写速度差距很大时,就插入一个高速缓存存储器,这样的思想就形成了存储器层次结构。引用《CSAPP》书中的图:
高速缓存(cache)存储器_第1张图片

而图中每一层都是下一层的缓存,也就是说,层次结构的每一层都缓存来自较低一层的数据。
接下来介绍块的概念,缓存存储器是分块的,数据总是以块为基本单位在每一层之间传递,块的大小只在相互传递的两层之间是相同的,并且一般是越位与底层的块大小越大,这样就弥补了底层存储器每次访问的所花销的大量的时间。

数据是缓存好了,但是不一定缓存的数据就是需要的数据。

所以取数据时也分缓存命中和缓存不命中。

缓存命中

意思就是第k+1层需要的数据就是第k层缓存好的,直接读取就行了。

缓存不命中

意思就是第k+1层需要的数据在第k层中没有。发生缓存不命中之后,会把该数据缓存在第k层中,如果当前有空余的空间还好,可以直接缓存,但是如果没有空间就会导致替换的发生,意思就是覆盖第k层中的某一个块,当然,这个不是随便替换的,也会使用适用于当前情况的替换策略,可以随机替换、也可以根据在某个时间段内引用次数最少的块来替换、还有替换掉最长时间没有使用的块,这些替换策略有的简单,有的复杂,复杂一点的,相对而言更加合理,但是还要考虑到复杂的替换策略所需要的代价。像磁盘这种读写速度很慢的存储器,就可以使用复杂一点的替换策略,替换策略所花的代价远比再次发生不命中所花的代价小。

缓存不命中也分为几大类:

  1. 强制不命中:即如果第k层的缓存是空的,那么一定就会不命中了。
  2. 冲突不命中:假如发生了不命中之后,数据是会被缓存在第k层的,那么缓存的位置该怎么选择呢,如果使用随机放置的方法,下次找起来不太方便,所以硬件通常将第k+1层中某个块限制在第k层中的某个子集中(采用映射),比如进行取余运算选择缓存位置,那么块号取余之后数字相同的块就会缓存到同一个地方,就发生了冲突,举个例子,假如进行mod3运算,第k+1层的块号为4、7的数据块需要缓存,这样都会缓存到第k层的块号为1的位置上。还有一个很糟糕的现象,即按照4、7、4、7这样的顺序来读数据,每次都会发生冲突不命中,就算第k层的空间还够,这种现象称为抖动
  3. 容量不命中当第k层的容量不足以缓存下需要缓存的数据时,就称为容量不命中。

另外,在存储器层次结构中,管理缓存的对象也不完全相同。比如编译器就管理寄存器,而SRAM的缓存由硬件管理,主存(DRAM)由操作系统和地址翻译硬件mmu管理。

前面都只介绍了一下缓存的大致的概念,而到底是怎么找寻数据对应的缓存的,以及高速缓存存储器的结构是什么,都还不知道,下面就进行说明。

这里继续引用《CSAPP》书中的图:
高速缓存(cache)存储器_第2张图片

考虑到一个计算机系统,其中每个存储器地址有m位,形成M=2^m个不同的地址。如图6-27a所示,这样一个机器的高速缓存被组织成一个有S=2^s个高速缓存组(cache set)的数组。每个组包含E个高速缓存行(cache line)。每个行是由一个B=2^b字节的数据块(block)组成的,一个有效位(valid bit)指明这个行是否包含有意义的信息,还有t=m-(b+s)个标记位(tag bit)(是当前块的存储器地址的位的一个子集),它们唯一地标识存储在这个高速缓存行中的块。

上面是《CSAPP》书中对图中的各数值的讲解。其实我们不用太关注每个占多少位什么的,只用知道,高速缓存分成组,每组又包含很多行,每行由有效位和标记位以及数据块组成的即可,这样不知道字母对应的哪个项再查看即可。
先从组开始分析,高速缓存分成了多个组,每组包含多行,分组可以得到数据缓存的大概位置,从6-27b图中的可以得出,组索引专门就是用来找到数据缓存的相应的组的。
再看行,有效位是用来标记该行有缓存数据,标记位是用来匹配组内的具体的哪一行的,见图6-27b图,前t位也是是标记位,我们通过组索引找到数据对应缓存的那一组,再根据标记位找到数据缓存在哪一行的,最后通过块偏移找到数据的起始字节。

是不是感觉很简单。

根据每个组的高速缓存行数高速缓存可以分为直接映射高速缓存(每组只有一个高速缓存行)、E路组相联高速缓存(每组有E个高速缓存行)、全相联高速缓存(只有一个组)

直接映射高速缓存:

由于每组只有一个高速缓存行,所以只要通过组索引就可以得到是哪一行了,但是这并不代表数据就缓存在这一行中,还要查看有效位是否置位,并且要对比标记位是否相同,这几个条件都满足了,才代表数据就缓存在该行中,最后通过块偏移找到数据的起始字节并取出。见图:
高速缓存(cache)存储器_第3张图片
当发生不命中时,由于每组只有一行,所以直接用新取出的行替换当前行就行了。

组相联高速缓存:

和直接映射高速缓存寻找组一样,都是通过组索引寻找,但是找到高速缓存行就不一样了,因为组相联高速缓存每组有多行,所以高速缓存需要寻找该组内和地址的标记位相同的标记位。

发生不命中时,就需要使用之前我们提到的替换策略了。

全相联高速缓存:

全相联高速缓存中,地址就没有组索引这一项了,因为并不需要了,只有一个组了,其它的步骤和组相联高速缓存一样。由于全相联高速缓存只有一个组,那么当高速缓存行多了的时候,找起来其实是非常费力的,所以它只适合做小的高速缓存,比如虚拟存储器系统中的翻译备用缓存器TLB。

影响一个高速缓存的性能有很多,比如:

1. 高速缓存的大小:大了可以提高命中率,但是会增加命中时间。

2. 块的大小:大了可以利用程序可能存在的空间局部性,提高命中率。由于块很大,所以传送的时间也增加了,如果发生了不命中,代价就很大了。

3. 相联度:大了可以降低高速缓存发生冲突不命中时出现抖动的可能性,但是会增加命中时间(因为要查标记位),以及替换策略的复杂性也增加了。

4. 写策略:之前没有说明写策略,因为它很困难也充满了细节,这里简单说明一下:当高速缓存中更新了一个数据时,要接着更新低一层中该数据时,就发生了写,如果采用直写策略,即直接更新该数据,但是这样虽然简单,但是每次都会在总线上传输,就算可能该数据不会被使用,还有一种策略称为写回,即当前层的这个数据对应的行要被替换了,才更新低一层的该数据,这样可以降低总线流量,但是复杂性变高了,因为不得不在高速缓存行上添加一位是否被修改的位。如果写不命中时,有种方法叫写分配,即加载相应的低一层中的块到高速缓存中,然后更新这个高速缓存块。那么写策略会给高速缓存带来什么影响呢。如果采用直写,传送数据的时间就增加了,所以在低层次的存储器中,一般采用写回。

小结:

高速缓存存储器的缓存机制差不多就说到这了,我们了解了大概的思想以及如何去寻找缓存和缓存不命中时会发生什么。但是其实其中还有很多细节,需要自己去查阅相关资料以及细读《CSAPP》才能发现。比如,为什么组索引不采用高位地址,而是中间的地址?这是因为如果采用高位地址,会导致连续的块被映射到相同的高速缓存块。还有块和行的区别是什么?行是高速缓存中存储块以及其它信息的容器,块是高速缓存存储器和下一层存储器传输的基本单位。

为了尽量让缓存命中,在程序中就应该尽量提高空间局部性和时间局部性。这两者是什么,以及怎么提高,后面再写博文进行说明。

你可能感兴趣的:(Linux及计算机体系结构)