前面已经介绍了一大堆关于cache的技术,那有没有cache优化的方法呢?毕竟我们的应用最终还是要通过优化来增加性能,下面介绍6个缓存优化方法
前面我们提到过,平均内存访问时间可由公式表示:
$$\begin{array}{l}
Average \ memory \ access \ time = Hit \ time + Miss \ rate \times Miss \ penalty
\end{array}$$
通过这个公式我们可以很方便看出,有3个指标可以用来优化
- 减少未命中几率:更大的块、更大的cache、并且更高的相关性
- 减少未命中惩罚:多层次缓存、相比写入,读取有更大的优先
- 减少命中时间:cache命中减少地址转化过程
比较经典的方法自然是减少未命中几率,我们将未命中细分为3块:
- 必定原因丢失:这一未命中出现在最开始当cache里面没有东西时,往cache里读取自然会未命中,直到cache中被存入指令和数据,这一块未命中时必定的
- 容量原因丢失:因为缓存不能在程序执行过程中包含所有需要的块,所以当缓存中块被丢弃其后又被检索到时就会发生容量丢失
- 冲突原因丢失:当块的置换策略(具体可看当遇到cache miss时的替换策略)为组相联或者是直接映射时,当有多个block被同时映射到某个set时,cache会发生替换,所以之前的数据丢失并且其后又被检索,这种未命中称为冲突命中
从表面上看容量原因丢失和冲突原因丢失很像,都是因为之前的数据被cache丢失而又被检索导致的,但是容量原因丢失和冲突原因丢失还是有不同的,先极端的假设一下,程序运行所需的内存大小小于cache的大小,这种情况下,容量原因丢失可以直接去掉,而冲突原因丢失还是会出现,当程序所需的内存在映射到缓存时都在同一个set或者block就会发生丢失
介绍完上面的前置知识,现在可以提出几个优化方法了
更大的块来减少未命中几率
前面我们提到过,缓存依赖于两个局部性:临时局部性和空间局部性,最近的使用的数据很大概率会再被使用,所用到数据的周围数据很大概率接下来被用到,比如for循环,for循环就是不断往后走,所以先直接将所需数据的后面数据全部装到cache中,运行到后面所需的数据就会存在于cache中,这会优化不少性能。
所以增加更大的块就是使用空间局部性的原理,原本块大小为16bytes,只能存后续16位的数据,超过就又要从内存中取,现在我们增加到64bytes,一下子可以存64位的数据,从内存中取数据的频率就变低了
但是这种操作自然会带来不好的一面,\( Miss \ penalty \)会变大,由于块大,当遇到不是空间局部性时候,比如if操作或者是for循环结束时。我们需要将这个块清除然后重新从内存取。因为块变大,相比于之前,我们这次清除加重取的操作成本会变大,\( Miss \ penalty \)就变大
更大的cache来减少未命中几率
增加cache大小意味着有更多的block,减少了容量原因丢失,同时也带来了更长的命中时间和更高的能耗,但是相比于劣势,这个方法的优势更突出,所以这种方式的使用较为广泛
提高相联性来减少未命中几率
提高相联性可以降低冲突原因丢失,假设一种情况,有8bytes的cache,我们从内存中访问8、16、24等与8成倍数的数据,每取完后面一个接着请求前面的数据,如果是这样一种访问顺序:8、16、8、24、16、32、24、32。在直接映射中,miss rate为100%,因为它们都映射在同一block上,会将之前的数据丢弃,后面在访问时就会未命中,当在二路相联时,miss rate就变为50%了。所以说提高相联性可以减少未命中几率
同样的,提高相联性会增加命中时间,提高了地址转化的复杂性
多层次存储降低未命中惩罚
如果是只有一层缓存的话,缓存未命中就会从内存中读取数据,这样未命中惩罚与内存的命中时间成正比,而我们都知道内存的访问时间需要上百时钟周期,所以在一级缓存和内存之间再添加一层缓存,这层缓存大小比一级缓存大一些,访问时间比一级缓存慢但是快于内存时间,一般称为二级缓存,这样可以将一级缓存未命中惩罚分摊在二级缓存上
这种方式的缺点是成本,毕竟如果可以的话将内存带宽速率跟上一级缓存会更好😂
优先读取 Misses 而不是 Writes 以减少 Miss 惩罚
之前我们讲到过,为了减少写入停顿,采用了write buffer,但是这样就有一个问题,如果在写入一个地址后紧跟一个读取这个地址的操作,这时写入的内容还在write buffer,读取的内容是错误的。
在直写情况下,最为简单的方法就是等待write buffer写入完成,write buffer变空。另一种方法就是当遇到read miss时读取write buffer的内容,不等待write buffer写入,这种方式就称为增加读优先级,通过减少Miss惩罚,不用等待write buffer写入,这种方法经常被使用。
在写回情况下,当write buffer写入时还只写入缓存块中,通过一位脏位来说明被修改,直到被替换才会写入内存,这种情况连第一种方式都不能使用,因为写入仅写到缓存中,内存中还没有改变,read miss需要等待被替换才能读到数据。所以只能使用读取write buffer的内容