前一节初步介绍了高速缓存的结构和地址划分策略,以及高速缓存“读”处理规则,这一节从讨论“写”开始。在讲解之前,先来复习两个概念:
时间局部性(Temporal Locality):正在被访问的数据,近期它很可能被再次访问。(比如抖音推送近期喜爱的专题)
空间局部性(Spatial Locality):正在被访问的数据,其存储空间的相邻空间在短期未来也可能被访问。
两个概念都针对正在被访问的信息而言。区别是时间局部性强调被访问的数据本身,而空间局部性强调存数据的空间本身。
一、高速缓存写的处理
缓存处理读的过程是,根据编号查找相应的值,如果不命中,就从下一集缓存调入新的数据,再根据替换策略(不细数),将新数据替换缓存中的旧数据。而对于缓存处理写的情况,稍复杂些。
假如CPU要对缓存中某个存在的块进行写操作,什么时候才去更新内存里的与之对应的该字段呢?如果内存和缓存同时更新,这称为直写,即高速缓存只是用来增加“读命中”,而不关心“写命中”。但如果都这样设计,那写操作对于缓存设计又有什么优势呢?所以更常见的方法是,你这个块先委托缓存保管,你写你的,修改你的。什么时候等该块要被驱逐出缓存时,再更新到内存,这称为写回,当然也可能在系统总线空闲时写回,总之写回是有条件的更新。从缓存的思想上看,这是非常美妙的策略,增加了时间局部性优势,减少低层存储设备的读取时间。但是,实现写回意味着更复杂的逻辑,比如每个缓存行都要维护一个修改位用于识别块是否被修改。
还有个问题,如果写不命中呢?CPU修改缓存里的某个字段时,发现这个字段不在缓存内,这时到底是从内存把字段读进缓存再修改缓存里的块,还是直接修改内存呢?前者利用空间局部性,但增加步骤,称为写分配,可以理解为分配缓存进行写;同理,后者就成为非写分配,即避开高速缓存直接写到下级存储器。很明显,直写一般就对应非写分配,而写回就对应写分配。有人问直写不是内存缓存一起写么?个人立即是因为直写只是保证内存和缓存不出现矛盾,而不矛盾的情况有两种:要么该数据在缓存和有内存中都有,那必须一致才不矛盾;要么缓存里压根没内存中的数据,这也叫做不矛盾:)
一般来说,写回+写分配的策略是主流,随着逻辑电路密度的提高,高复杂性的实现越来越容易。虽然采用直写实现起来更简单,但是增加了总线事物,存储层次越往下走,数据传送的耗时越长,因此越可能采用写回而不是直写策略。
二、高速缓存分配策略
我们要讨论缓存对程序优化的影响,就要考虑缓存的不命中率(miss rate),还要考虑各级存储的不命中率处罚。一般L1不命中,需要5~10个周期从L2获得数据;L2和L3类似,而缓存不命中从内存获得数据则需要耗费25~100个周期。
高速缓存的总大小与性能的关系可以做定性讨论。一方面,高速缓存越大,命中率肯定更高;但另一方面,高速缓存更大,命中时间也越长,因此不同的缓存可能采用不同的策略。比如L1,要求不命中处罚时间只能几个周期,那么L1就不能采用大缓存,否则当L1缓存大到一定境界,查找时间都接近不命中处罚时间了。
高速缓存的块大小和行数量(相联度)与性能的关系也可以做定性讨论。一方面,有较大的块,每行存的数据就更大,而缓存中这些不同的块可能对应完全不相干的低层存储区域,相邻行之间的块数据可能毫不相干,因此本行的数据越大,则代表某个区域的数据集体被缓存的就越多,这是利用了空间局部性(相邻空间大概率被连续访问,其实也可以理解成顺序局部性);但另一方面,高速缓存总大小一旦确定,块越大就意味着行越少,行越少,就意味着缓存可能映射更少计算模块种类的低层存储区域,这就破坏了时间局部性(比如在多进程调用中,你始终只照顾了少数进程模块的计算并用大块给予良好的空间局部性,但大部分时间其他进程模块频繁造成命中,时间局部性就很差)。所谓优化时间局部性即当缓存某块数据后,在短时间内尽可能多重复使用这些数据,而CPU调用的数据可能涉及到存储区很多不同区域,如果你的行太少,覆盖的各存储区域就不够广泛,自然也就很难保证更多区域的数据能够及时被缓存并且被重复使用,即便你利用空间局部性在缓存某几个区域数据时让他们被缓存得更完整。当应对不同数据处理顺序导致某些情况造成不命中的区域增多时,程序实际运行效率参数就可能出现明显的抖动现象。事实上,时间局部性比空间局部性有更好的命中率,如果从命中率来考虑,那么时间局部性的优先级更高。
然而我们不能无限制的提高行数(提高相联度参数E),如果行数太多,需要更多的标记位,实现起来更昂贵并且访问速度很难提高,实现复杂度的增加会使得其命中时间加大,而且不命中处罚也很大,因为选择牺牲行(缓存慢时需要清除旧行移入不命中新行)的复杂性也增加了。
以上的分配策略讨论,也是CPU生产商经过权衡依据,他们在设计L1、L2甚至L3时,会反映出他们的折中结果,而且这个结果除了依据理论推导外,还会引入大量的程序运行实践。
三、直接映射高速缓存命中率问题的模拟
这里详细套用教材上两个很有意思的练习题。
1、转置矩阵行列互换的实现:
typedef int array [2][2];
void transpose(array dst, array src)
{
int i, j;
for(i = 0; i < 2; i++){
<