Cache的作用
首先,Cache是CPU和内存之间的桥梁,为了解决当前CPU高主频和内存读写速度不匹配的问题。
这座桥梁当然能够被绕过去,这样CPU会直接从内存中读写数据。一般地整个内核空间,我们会看到:
kseg0(地址空间0x80000000-0x9FFFFFFF)是cached/unmapped
kseg1(地址空间0xA0000000-0xBFFFFFFF)是uncached/unmapped
kseg2(地址空间0xC0000000-0xFFFFFFFF)是cached/mapped
访问uncached空间一定会降低CPU的性能,所以只有当Cache还没有被初始化时才会特意通过访问uncached空间。
Cache的实现机制
现代MIPS基本都采用两级Cache,第一级Cache离CPU近,容量小,第二级Cache离CPU远,容量大
第一级Cache分离:ICache和DCache。实现方式分为直接映射/2路相联/4路相联/8路相联
直接映射/多路相联
直接映射方式下,同一个内存地址对应同一个Cache索引。多个不同的内存地址对应同一个Cache索引。
例如当Cache大小为8,内存大小为64时,内存地址0、8、16等对于Cache地址0。这种方式带来了一个问题:当频繁访问内存地址0、8,Cache地址0中的数据需要频繁更新。
多路相联可以解决直接映射带来的问题。当使用2路相联时,频繁访问地址0、8不会频繁更新Cache地址0。因为有可能第1路Cache地址0存储内存地址0的数据,第2路Cache地址0存储内存地址8的数据。
一般情况下,4路相联是最具有性价比的。
写操作分为:回写和透写
Cache的hit和miss
以16K大小Cache,使用4路相联,Cache中每行映射32字节为例。每路Cache可以看作是一个结构体数组,数组包括256行,每行的格式为:Tag,32字节映射数据。
Tag(标签地址)
32Byte Data(缓存的32字节数据)
(实际情况要复杂一些,每行Cache还需要有一个状态位,用于标记这个Cache行是否Dirty)
具体命中过程:
A、得到索引地址:
程序地址(虚拟地址)低12位除以32。
Index= VAddr[11:0] / 32
例如当低12位值为120时,index值为3,对应256行Cache单元的第3行
其中低12是因为每路缓存4K,若是每路8K,则需要取低13位。
B、得到Tag标签:
程序地址经过转换得到物理地址,物理地址的高20位为Tag
C、匹配Tag标签
查看第3行Cache单元的Tag是否和上步得到的Tag相等,相等则hit, 不相等则Miss。当然,多路相联方式需要对每路进行Tag匹配。
Cache在Miss情况下更新Cache
以4路为例,当4路Tag都未命中时,需要从内存中读取32字节数据(读取数据的起始地址需要32字节对齐),并将32字节数据更新到指定的Cache行(由索引地址指定)。
不过我们有4路相联,到底更新哪一路?一般采用的LRU算法(当然还有其他算法,例如随机更新)
Cache如何处理写操作
当Cache行中的数据比对应内存中的数据要新时,我们需要将Cache中的数据写入到内存中。
早期MIPS采用透写方法:CPU在写内存时会同时更新Cache和内存。这样做的缺点是,CPU的性能会受内存性能极大影响。
现代MIPS基本采用回写方法:对于在Cache中没有地址的内存,直接写入到内存(例如一些IO操作)
或者先将内存中的数据读取到Cache中,再直接写Cache(写分配)
Cache重影问题(第一级的Index使用虚拟地址,所以只对第一级Cache有影响)
简单说,因为2个不同的虚拟地址可能对应同一个物理地址,且这2个虚拟地址产生不同的索引地址。那么就有可能在Cache中不同的两个位置缓存中同一个物理地址中的数据。从而导致更新其中一个Cache行,另外一个Cache行不知道。
解决办法(当MMU以4K为单位进行管理为例):
A、每路Cache的大小<=4K
B、或者当每路Cache超过4K时,对于同一个物理地址的两个虚拟地址之间的差值必须是每路Cache的整数倍。(这样能够保证对应同一个物理地址的两个虚拟地址计算得到的索引地址是同一个)。
管理Cache
只有几种情况需要程序员对Cache进行干预:
DMA读写操作。原因是DMA和CPU独立。
将指令拷贝到内存然后执行,这种情况下,需要清除ICache,原因是DCache和ICache互为独立。因此需要DCache回写以及ICache作废。
Cache指令相关
Cache指令的格式是 Cache Op, address。 address是虚拟地址,Op表示操作类型(5bit)
OP可以指定Cache L1/L2/L3
也可以指定操作类型:命中,地址/寻址,索引
编程注意
Cache初始化时,先初始化ICache,再初始化DCache。
软件编程,尤其是算法移植时,需要有效利用时间局部性和空间局部性。具体的做法是:顺序读内存乱序写内存VS顺序写内存乱序读内存。前者能够更加有效利用Cache。同一地址内存读操作尽量只做一次,处理完毕后再更新内存。