缓存的基本原理
计算机通常将每一层的空间划分成若干字节的数据块,在第k层与第k+1层之间,以数据块为单位进行数据传输。
为什么要使用块而不是更小的单位(比如一个字节)呢?主要有下面两方面的考虑:
1. 空间局部性。由于编写良好的程序倾向于具有空间局部性,所以每次读取数据时,不仅仅读取当前所需的的内容,还要将包含该数据以及其邻近数据的整个块都读入到缓存中,这样在不久的将来需要读取邻近数据时,就可以直接从当前缓存中读取,从而提高了访问速度。
2. 性能考虑。由于读取设备需要许多必要的开销,比如查找设备、寻址等等,如果一次只读取一个字节,那么读取N个字节就需要重复N次这些耗时的操作,所以非常不划算。所以从性能上考虑,也倾向于一次读取读取更多的数据以减少不必要的开销。同时,在层次存储结构中,越往下走,设备访问速度也越慢,所以越低层的设备倾向于一次读取更多的数据。
CPU高速缓存
CPU高速缓存结构是在整个层次存储结构中最重要的一环。通常术语cache常常指代的就是介于CPU和主存间的高速缓存。大体上,它的结构如下图所示:
从图中我们可以看到,L1 cache(也就是我们通常所说的一级缓存)被划分为两个独立的部分:L1i (L1 instruction cache,指令缓存)和L1d (L1 data,数据缓存)。之所以要这样做,是处于提高并行性能的考虑,使得CPU可以并行的进行取指令及读取数据的操作了,从而避免等待。这样的结构,被称为Harvard architecture。
由于主存空间往往大于高速缓存,所以只有部分的主存块能存放在高速缓存中,应此,需要采用一定的映射策略来决定哪些主存块应该存放在高速缓存中,以及存放的具体位置。
在解决映射策略前,需要按一定的方式来组织高速缓存空间:
1. 首先,将缓存组织成有S个高速缓存组(cache set)的数组;
2. 其中,每个组包含了E个高速缓存行(cache line);
3. 每行拥有由一个B (=2b)byte大小的数据块(data blocks,通常为64 Kbyte),一个用于唯一标识该行的tag字段(相当于该行的地址),一个有效位(valid bit)。
如下图所示。
可以看到,实际用于存储数据的缓存大小为:C=B*E*S 字节。
好了,对于一个32位的计算机(地址总线共32条),CPU如何通过一个32位的地址找到对应的缓存块呢?
对于32bit的地址,将末端b(b=logB)位作为块内偏移地址,s(s =logS)位作为组索引,用于寻找对应的高速缓存组,剩下的t(t=m-s-t,m为地址位数,此处为32)位,作为标记位(也相当于行索引位),用于寻找对应的行。
首先根据s位的组索引,找到对应的组(相当于在一个组数组(set array)中按下标进行查找),其次在该组中,找出Tag标志与地址中Tag标志相同的行,该行即为要找的行,在该行的数据块中以偏移地址为起始地址读取数据。
对应于不同的组和行的划分,有以下三种不同的映射的方式:
1.直接映射
直接映射相当于每一组只有一行,即行数E=1。也就是一个组就相当于一个行,一个行相当于一个组。
之所以叫直接映射,是因为对于每一个给定的地址,都只有唯一确定的一个高速缓存行与之对应(因为由组索引号就已经决定了其行索引号)。如下图:
那么对于任意两个中间s位相同的地址,按照直接映射的策略,就会对应到同一个高速缓存行(组),这个时候,我们就需要将地址中t bit的Tag标志与该高速缓存行的Tag标志进行比较,若相同则表明访问命中,反之则不命中,此时的“不命中“就称为冲突不命中,即明明高速缓存中又空闲的缓存行,但是由于映射策略引起了冲突从而造成了不命中。
2.全相联映射
与直接映射相比,全相联映射是另一个极端,前者只允许每个地址映射到唯一的缓存行,那么好了,全相联映射则让你可以将某个地址映射到高速缓存行中任意一行。具体的说就是:整个cache只划分成一个组(相当于没有分组),该组内有若干行,即组数S=1 ,行数E=C/B。
由于不需要进行组的选择,所以整个地址只需要划分成块内偏移以及Tag标志两个字段:
那么,通过将地址中的Tag标志与每一个高速缓存行的Tag标志进行比较,找出相同的,即为所求行。再根据块偏移读取相应数据。
3.组相联映射
组相联映射是直接映射和全相联映射的折中,它将高速缓存划分为S组,每组有E(