持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情
4 缓存块替换策略
目标:被替换出的数据块应该是将来最晚会被访问的块。但对将来事情无法预测,因为处理器不知程序将来访问哪个地址。因此,现在的缓存替换策略都采用最近最少使用算法(Least Recently Used ,LRU)或类LRU算法。
如程序要顺序访问 B1 、B2、B3、B4、B5地址块,这几个缓存块都映射到缓存的同一组,同时假设缓存采用4路组组相连映射,则当访问B5时,B1就需被替换出来。最简单的利用位矩阵实现。
先定义一个行、列都与缓存路数相同的矩阵。当访问某路对应缓存块,先将该路对应的所有行置为1,然后再将该路对应的所有列置为0。最终结果体现为,缓存块访问时间的先后顺序,由矩阵行中1的个数决定,最近最常访问缓存块对应行1的个数最多。
假设现在一个四路相连的缓存组包含数据块 B1、B2、B3、B4, 数据块的访问顺序为 B2、B3、B1、B4,则LRU矩阵在每次访问后的变化:
最终B2对应行的1的个数最少,所以B2将会被优先替换。
5 缓存对程序性能的影响
CPU将未来最可能被用到的内存数据加载进缓存。若下次访问内存时:
- 数据已在缓存中,即缓存命中,它获取目标数据的速度很快
- 若数据不在缓存,即缓存缺失,此时要启动内存数据传输,而内存访问速度相比缓存差很多。所以要避免这种情况
哪些情况易缓存缺失及程序性能影响。
5.1 缓存缺失
缓存性能主要取决于缓存命中率,缓存缺失(cache miss)越少,缓存性能越好。
引起缓存缺失的类型:
① 强制缺失
第一次将数据块读入缓存所产生缺失,也称冷缺失(cold miss),因为当发生缓存缺失时,缓存是空的(冷的)。
因为第一次将数据读入缓存时,缓存不会有数据,这种缺失无法避免。
② 冲突缺失
由于缓存的相连度有限导致的缺失。
③ 容量缺失
由于缓存大小有限导致的缺失。可认为是除了强制缺失、冲突缺失外的缺失。当程序运行的某段时间内,访问地址范围超过缓存大小过多,这样缓存容量就会成为缓存性能瓶颈。
注意和冲突缺失区别:
- 冲突缺失指同组内的缺失
- 容量缺失描述范围是整个缓存
第②类冲突缺失因为相连度有限。第一步可通过getconf查看缓存信息:
# 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 3145728
LEVEL3_CACHE_ASSOC 12
LEVEL3_CACHE_LINESIZE 64
LEVEL4_CACHE_SIZE 0
LEVEL4_CACHE_ASSOC 0
LEVEL4_CACHE_LINESIZE 0
这缓存信息中,L1Cache(LEVEL1_ICACHE和LEVEL1_DCACHE分别表示指令缓存、数据缓存)的cache line 大小为64字节,路数为8路,32K,可计算出缓存组数为64组()。
第二步,我们使用一个程序来测试缓存的影响:
// cache.c
#include <stdio.h>
#include <stdlib.h>
#define M 64
#define N 10000000
int main( )
{
printf("%ld",sizeof(long long));
long long (*a)[N] = (long long(*)[N])calloc(M * N, sizeof(long long));
for(int i = 0; i < 100000000; i++) {
for(int j = 0; j < 4096; j+=512) {
a[5][j]++;
}
}
return 0;
}
上面代码中定义了一个二维数组,数组中元素的类型为long long ,元素大小为8字节。所以一个cache line 可以存放 =个元素。一组是8路,所以一组可以存放=个元素。一路包含64个cache line,因为前面计算出缓存的组数为64,所以一路可以存放=个元素。
代码中:
- 第一层循环,执行次数
- 第二层循环,以512 为间隔访问元素,即每次访问都会落在同一个组内的不同cache line ,因为一组有8路,所以我们迭代到 =的位置。这样可以使同一组刚好可以容纳二层循环需要的地址空间。
运行结果:
# gcc cache.c
# time ./a.out
8
real 0m2.670s
user 0m2.671s
sys 0m0.001s
第三步,第二层循环的迭代次数扩大一倍:
# gcc cache.c
# time ./a.out
8
real 0m16.693s
user 0m16.700s
sys 0m0.001s
虽然运算量增加了一倍,但运行时间增加6倍,性能劣化三倍。劣化根本原因,当i > 4096,即访问4096后的元素,同组的cache line已全部使用,须替换,且之后的每次访问都会冲突,导致缓存块频繁替换,性能劣化严重。