cup缓存基础知识
cup缓存
CPU缓存(CPU Cache)的目的是为了提高访问内存(RAM)的效率,这虽然已经涉及到硬件的领域,但它仍然与我们息息相关,了解了它的一些原理,能让我们写出更高效的程序,另外在多线程程序中,一些不可思议的问题也与缓存有关。
现代多核处理器,一个CPU由多个核组成,每个核又可以有多个硬件线程,比如我们说4核8线程,就是指有4个核,每个核2个线程,这在OS看来就像8个并行处理器一样。
CPU缓存有多级缓存,比如L1, L2, L3等:
L1容量最小,速度最快,每个核都有L1缓存,L1又专门针对指令和数据分成L1d(数据缓存),L1i(指令缓存)。
L2容量比L1大,速度比L1慢,每个核都有L2缓存。
L3容量最大,速度最慢,多个核共享一个L3缓存。
有些CPU可能还有L4缓存,不过不常见;此外还有其他类型的缓存,比如TLB(translation lookaside buffer),用于物理地址和虚拟地址转译,这不是我们关心的缓存。
下图展示了缓存和CPU的关系:
Linux用下面命令可以查看CPU缓存的信息:
[root@sg-14 ~]# getconf -a | grep CACHE
LEVEL1_ICACHE_SIZE 32768
LEVEL1_ICACHE_ASSOC 8
LEVEL1_ICACHE_LINESIZE 64
LEVEL1_DCACHE_SIZE 32768
LEVEL1_DCACHE_ASSOC 8
LEVEL1_DCACHE_LINESIZE 64
LEVEL2_CACHE_SIZE 262144
LEVEL2_CACHE_ASSOC 4
LEVEL2_CACHE_LINESIZE 64
LEVEL3_CACHE_SIZE 6291456
LEVEL3_CACHE_ASSOC 12
LEVEL3_CACHE_LINESIZE 64
LEVEL4_CACHE_SIZE 0
LEVEL4_CACHE_ASSOC 0
LEVEL4_CACHE_LINESIZE 0
上面显示CPU只有2级缓存,L3,L4都为0。
L1的数据缓存和指令缓存分别是32KB;L2为2048KB,L3位6MB。
在缓存和主存之间,数据是按固定大小的块传输的 该块称为缓存行(cache line),这里显示每行的大小为64Bytes。
ASSOC表示主存地址映射到缓存的策略,这里L1,L2是8路组相联,L3是12路组相联,等一会儿再说是什么意思。
缓存结构
一块CPU缓存可以看成是一个数组,数组元素是缓存项(cache entry),一个缓存项的内容大概是这样的:
+-------------------------------------------+
| tag | data block(cache line) | flag |
+-------------------------------------------+
data block就是从内存中拷贝过来的数据,也就是我们说的cache line,从上面信息可知大小是64字节。
tag 保存了内存地址的一部分,是用来验证是否缓存命中的。
flag 是一些标志位,比如缓存是否失效,写dirty等等。
实际上LEVEL1_ICACHE_SIZE这个数据,是用data block来算的,并不包括tag和flag占用的大小,比如64 x 512 = 32768,表示LEVEL1_ICACHE_SIZE可以缓存512个cache line。
缓存首先要解决的问题是:怎么映射内存地址和缓存地址?比如CPU要检查一个内存值是否已经缓存,那么它首先要能算出这个内存地址对应的缓存地址,然后才能检查。
为了解决这个问题,缓存将一个内存地址分成下面几个部分:
+-------------------------------------------+
| tag | index | offset |
+-------------------------------------------+
tag和缓存项中的tag对应,用来验证是否缓存命中的。
index 缓存项数组中的索引。
offset 缓存块(cache line)中的偏移,因为缓存块是64字节,而内存值可能只有4个字节,一个缓存块可以保存多个连续的内存值。这个offset实际上就是指明内存值在cache line中的位置。
直接映射缓存
现在我们举一个具体的例子,说明内存和缓存是如何映射的:
-
假如缓存的大小是32768B(32KB),缓存块大小是64B,那么缓存项数组就有 32768/64=512 个。
-
CPU要访问一个内存地址
0x1CAABBDD
,它首先检查这个内存地址是否在缓存中,检查过程是这样的: -
内存地址的二进制形式是(低位在前面):
| tag | index | offset | 0 0 0 1 1 1 0 0 1 0 1 0 1 0 1 0 1 0 1 1 1 0 1 1 1 1 0 1 1 1 0 1
先计算内存在cache line中的偏移,因为缓存块是64字节,那么offset需要占6位(2^6=64),即offset=011101=29。
接着要计算缓存项的索引,因为缓存项数组是512个,所以index需要占9位(2^9=512),即index=011101111=239。
现在我们通过offset和index已经找到缓存块的具体位置了,但是因为内存要远比缓存大很多,所以多个内存块是可以映射到同一个位置的,怎么判断这个缓存块位置存的就是这个内存的值呢?答案就是tag:内存地址去掉index和offset的部分,剩下的就是tag=00011100101010101=0x3955。
通过index找到缓存项,比较缓存项中的tag是否与内存地址中的tag相同,如果相同表示命中,就直接取缓存块中的值;如果不同表示未命中,CPU需要将内存值拷贝到缓存(替换掉老的)。
这种映射方式就称为直接映射(Direct mapped),它的缺点就是多个内存地址会映射到同一个缓存地址,拿上面的内存地址来看,只要offset和index相同的内存地址,就一定会映射到同一个地方,比如:
00011100101010100 011101111 011101
00011100101010110 011101111 011101
00011100101010111 011101111 011101
如果同时访问上面3个地址,就会一直替换缓存的值,也就是一直出现缓存冲突,这可能比没有缓存还要慢,因为除了访问内存外,还多一个拷贝内存值到缓存的操作。
选择了IT,必定终身学习