前面的各种存储器大概看了一下,DRAM和SRAM是理解的,其他的存储器就朦朦胧胧知道是怎么回事,就不深入了,这里主要做一下Cache的笔记。 @[toc]
一、何为Cache
一个Cache,由S组(sets)组成,每组都有E块(blocks)数据块,每个数据块都有B个字节(bytes),所以总缓存大小C=S*E*B data bytes.
这里的块也就Cache中的一行嘛,每次传输一整块的数据。
注2:
!!!block和line是同个意思!!!看CSAPP的时候困惑了好久,一会block一会line,原来是一样的。那么这里就统一用block来表示
1.直接映射缓存(Direct-Mapped Cache)
最简单的情况,S=2^s sets,E=1blocks per set,B=2^b 字节 per block,且地址位数为t+s+b,那么得到的一个地址可以如下图这样划分:
所以找某个地址的数据的时候,传入地址,将地址上面的方式切分成
tag标记位,set index组号,和block offset块内偏移。
如上图,就是E=1的一个示例。每次进来一个地址,都剥离出tag,set index 和 block offset,先根据set index找到那一组,由于每个组只有一块,所以只需要比对找到的那一块记录的标志位是否与传入的tag相等,若相等且valid=1,则表示该数据在缓存中,直接从缓存中取走数据即可,否则表示没在缓存中。
这叫
直接映射缓存,因为每次来,根据set就直接找到那个block了(因为one set 就只有 one block),然后直接和那个比对即可。 这种缓存有个明显的缺点,就是如果AB的set index相同,然后程序的特点是ABABAB这样交错地访问的话,那么一次都不会命中。 所以更好的办法是增加E,使得每一组可以并行放好几个相同set index的block,在下面介绍。
2.组相联映射(E-way Set Associative CacheE路相联缓存)
E>1时,每组都有E块可供缓存,也就是说,相同set index的地址可以不用争夺那仅有的一个块了,现在有很多块可供大家选用。由于有选择的空间了,也就能够选择进来一个的时候覆盖在哪个旧数据块上了,也就开始需要淘汰算法了。
下面有一个2-Way 的例子。
每次进来一个数据时,同样分解为tag,set index和block offset,同样找到对应的set index那一行,然后去比较那一组上所有的块是否有对应的tag并且valid=1。
3.全相联映射
从直接映射到组相联映射,数据存于缓存哪个位置的自由性增加,这个自由性在全相联映射这里做到了极致。全相联映射,顾名思义,可以看作所有块都全部相联在同一行,甚至这里去掉了行的概念,仅仅设计了一行,地址只分成了tag和offset。每次都可以随便找个位置放,只要对应Cache行是空的或者符合某种替换规则,都可以映射。
显而易见,在相同缓存大小的情况的,能最大限度地利用所有的缓存空间来工作,有着最灵活的替换策略。但是缺点也很明显,就是每次传入一个地址来找是否在缓存中时,需要找遍所有的缓存块来看是否有相同的tag,很复杂。
二、写Cache
1.What to do on a write-hit
策略1:
Write-through写穿(write immediately to memory.):当数据写到某地址刚好命中缓存时,不仅修改缓存中的数据,而且立即将新数据传给主存,才会得到写完成的相应。
写穿策略显而易见的弊端是
效率低,又要写到cache,又要写到主存。策略2:
Write-back写回:defer write to memory until replacementof line.当数据写到某地址刚好命中缓存时,只会修改缓存中的数据,不会刷新到内存。直到那个被替换之时,才会刷新到内存。 对于写回,需要在行中加
dirty位来指示是否写入了这些块。算法:当缓存识别出某个块将被覆盖时,它检查dirty位(修改位),若为1(表示修改过了,dirty了),则将该数据写回磁盘。如果dirty是0,当然没有必要写入数据到内存。【直到最后时刻才写到磁盘】
2.What to do on a write-miss
策略1:
Write-allocate写分配:load into cache,update line in cache.加载到缓存,更新这个缓存行 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 策略2No-write-allocate非写分配:write straight to memory,does not load into cache.直接写到主存中,不加载到缓存中
目前基本都采取 :组相联映射+ 写回 + 写分配 的策略
三、存储器层次结构(Memory Hierarcchies)
在磁盘容量越来越大的情况下,我们的存储设备和CPU之间 存在的这种访问速度的差距,在很大程度上被程序局部性弥补。所以,存储技术的这些特性和我们的程序的局部性属性相互补充地非常完美,为人们提供针对怎样设计存储系统的建议——这种设计称为存储器层次结构Memory Hierarchy.
缓存不命中 cache miss
1.覆盖现存的块称为替换replacing或驱逐evicting
2.被驱逐的块称为牺牲块victim block
3.决定替换哪个块是有缓存的替换策略replacement policy来控制的
种类
四、Cache对程序性能的影响
1.存储器山(Memory Mountain)
读吞吐量Read Throughput,又叫读带宽Read Bandwidth,表示每秒从存储系统中读取的字节数(MB/s),常常用来衡量程序存储系统的性能。
存储器山Memory Mountain,是测量读吞吐量作为空间和时间局部性的函数。每台计算机都有表明它存储器系统的能力特色的唯一的存储器山,下图是存储器山的测试程序以及Intel Core i7系统的存储器山。
下面还有一个run函数,是实际测量存储器山的函数。
double run(int size,int stride,double Mhz)
{
double cycles;
int elems = size / sizeof(double);
test(elems,stride); //warm up the cache
cycles = fcyc2(test,elems,stride,0); //call test(elems,stride)
return (size / stride) / (cycles / Mhz);//convert cycles to MB/s
}
注:这个图是先暖好身的,已经跑过一遍后,第二次跑时的数据
1.固定Size不变<=32K(L1 cache的容量)时,当Stride增加时,吞吐量几乎不变,因为所有数据都在L1 cache中了,每次都是命中的。
2.固定Size不变>32K时,由于只有一部分存在L1 cache中了,Size越大的越在难存在缓存中,所以出现了空间局部性的斜坡.
3.固定步长不变,可以看到当元素总数量
工作集<=32KB时,读吞吐量几乎不变,因为所有数据都在L1 cache中,每次都命中。
工作集尺寸比32KB大一点且小于256KB时,由于读吞吐量受从.L2 cache读数据的比例增加而急剧下降,所以有了个悬崖。接近32+256KB时,由于速度基本被L2的速度给拉了,基本上就保持L2 cache的读速度了。
工作集尺寸比256KB大一点,小于8MB时,同样的道理有个悬崖,后面基本就保持L3 cache 的读速度了
工作集尺寸大于8MB,由于L3也存不下了,只能从主存中读,所以读速度又迅速下降,尺寸再大就基本保持主存的读取速度。
当工作集太大,不能装进任何一个高速缓存时,主存山脊的最高点(步长=1)也比他的最低点高8倍.所以,
即使当程序的时间局部性很差,空间局部性依然能补救,并且他是非常重要的。
2.重新排列循环以提高空间局部性
3.利用分块来提高时间局部性
下图是一个求矩阵乘积的代码,可以看到其只有a[in+k]有较好的空间局部性,而b[kn+j]的空间局部性很差。不命中率=n/8+n=9n/8 再乘以n^2= (9/8)n^3
因而考虑通过矩阵分块进行优化:
一下列举一下这节中的重要的点来结束这节课:
1.将你的注意力集中在内循环上,大部分计算的存储器访问都集中在这里; 2.通过按照数据实际在存储器中存放的顺序,以步长为1来访问,空间局部性最优; 3.一旦从一个存储器中读了一个数据出来,就尽可能多的利用他(kij版本). 4.当数据长度大到一定程度时,时间局部性就会变差。(因为下次再访问时可能已经被覆盖了)