根据Intel的优化手册,L1数据高速缓存为32 KiB,并与64字节行的8路关联。我编写了以下微基准测试内存读取性能。
我假设如果仅访问可容纳32 KiB缓存的块,则每个内存访问将很快,但是如果我们超过该缓存大小,则访问将突然变慢。当skip
为1
时,基准测试将按顺序访问每一行。
void benchmark(int bs, int nb, int trials, int skip)
{
printf("block size: %d, blocks: %d, skip: %d, trials: %d\n", bs, nb, skip, trials);
printf("total data size: %d\n", nb*bs*skip);
printf("accessed data size: %d\n", nb*bs);
uint8_t volatile data[nb*bs*skip];
clock_t before = clock();
for (int i = 0; i < trials; ++i) {
for (int block = 0; block < nb; ++block) {
data[block * bs * skip];
}
}
clock_t after = clock() - before;
double ns_per_access = (double)after/CLOCKS_PER_SEC/nb/trials * 1000000000;
printf("%f ns per memory access\n", ns_per_access);
}
再次使用skip = 1
,结果符合我的假设:
~ ❯❯❯ ./bm -s 64 -b 128 -t 10000000 -k 1
block size: 64, blocks: 128, skip: 1, trials: 10000000
total data size: 8192
accessed data size: 8192
0.269054 ns per memory access
~ ❯❯❯ ./bm -s 64 -b 256 -t 10000000 -k 1
block size: 64, blocks: 256, skip: 1, trials: 10000000
total data size: 16384
accessed data size: 16384
0.278184 ns per memory access
~ ❯❯❯ ./bm -s 64 -b 512 -t 10000000 -k 1
block size: 64, blocks: 512, skip: 1, trials: 10000000
total data size: 32768
accessed data size: 32768
0.245591 ns per memory access
~ ❯❯❯ ./bm -s 64 -b 1024 -t 10000000 -k 1
block size: 64, blocks: 1024, skip: 1, trials: 10000000
total data size: 65536
accessed data size: 65536
0.582870 ns per memory access
到目前为止,效果非常好:当所有内容都放入L1高速缓存中时,内部循环大约每纳秒运行4次,或者每个时钟周期运行一次以上。如果我们使数据太大,则需要花费更长的时间。这完全符合我对缓存应如何工作的理解。
现在,通过将skip
设为2
,来访问每个 other 块。
~ ❯❯❯ ./bm -s 64 -b 512 -t 10000000 -k 2
block size: 64, blocks: 512, skip: 2, trials: 10000000
total data size: 65536
accessed data size: 32768
0.582181 ns per memory access
这违反了我的理解!对于直接映射的缓存,这是有道理的,但是由于我们的缓存是关联的,所以我看不出为什么行之间应该相互冲突。为什么访问其他所有块的速度变慢?
但是,如果我将skip
设置为3
,事情又很快了。实际上,skip
的任何奇数值都是很快的。任何偶数值都很慢。例如:
~ ❯❯❯ ./bm -s 64 -b 512 -t 10000000 -k 7
block size: 64, blocks: 512, skip: 7, trials: 10000000
total data size: 229376
accessed data size: 32768
0.265338 ns per memory access
~ ❯❯❯ ./bm -s 64 -b 512 -t 10000000 -k 12
block size: 64, blocks: 512, skip: 12, trials: 10000000
total data size: 393216
accessed data size: 32768
0.616013 ns per memory access
为什么会这样?
出于完整性考虑:我使用的是运行macOS 10.13.4的2015年中期MacBook Pro。我完整的CPU品牌字符串是Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz
。我正在使用cc -O3 -o bm bm.c
进行编译;该编译器是Xcode 9.4.1附带的编译器。我省略了main
函数;它所做的只是解析命令行选项并调用benchmark
。
答案 0 :(得分:3)
缓存不是完全关联的,它是 set 关联的,这意味着每个地址都映射到某个集合,并且关联性仅在映射到同一集合的行之间起作用。
通过使步数等于2,您可以保留游戏中一半的游戏集,因此,有效访问32K的事实并不重要-您只有16k的可用空间(例如,偶数集),因此您仍然超过您的容量并开始尝试(结束从下一个级别获取数据)。
当步长为3时,问题就消失了,因为缠绕后可以使用所有组。对于任何素数都一样(这就是为什么它有时用于地址散列的原因)