分析Cache访存模式对系统性能的影响

表1、普通矩阵乘法与及优化后矩阵乘法之间的性能对比

矩阵大小 100 500 1000 1500 2000 2500 3000
一般算法执行时间 0.005 0.622 5.177 25.763 51.578 116.024 193.515
优化算法执行时间 0.004 0.384 3.070 12.480 22.462 49.304 82.696
加速比speedup 1.337 1.620 1.686 2.064 2.296 2.353 2.340

加速比定义:加速比=优化前系统耗时/优化后系统耗时;

所谓加速比,就是优化前的耗时与优化后耗时的比值。加速比越高,表明优化效果越明显

分析原因:

传统的矩阵乘法算法通过遍历结果矩阵 c 的每一行和每一列来计算每个元素的值。在这种访问模式下,矩阵 a 的访问步长为 1,表现出良好的空间局部性,即连续访问的内存地址相邻,有利于缓存命中。

然而,矩阵 b 的访问步长为 size,意味着每次访问的内存地址间隔较大,导致缓存命中率较低。

image-20240614144958987

为了优化缓存性能,我们可以采用一种改进的访问模式。通过遍历矩阵 a 的每个元素,将每个元素对结果矩阵 c 的贡献累加到对应位置,从而实现对矩阵 b 的连续访问,即步长为 1 的访问模式。这种优化策略有效地提高了矩阵 b 的缓存命中率,从而显著提升矩阵乘法的性能。

从这个矩阵乘法的例子中,我们可以看出访问模式对缓存性能具有非常显著影响。

Cache层次和L1cacheline测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
#include<string.h>

std::random_device rd; // 随机数生成
std::mt19937 gen(rd());

std::vector<unsigned int> sizes{8, 16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192, 10240, 12288, 16384};
std::vector<int> strides{ 1,2,4,8,16,32,64,96,128,192,256,512,1024,1536,2048 };

void test_cache(int size){
int n = size / sizeof(char);
char *arr = new char[n]; //申请存储空间
memset(arr, 1, sizeof(char) * n); //初始化为 1
std::uniform_int_distribution<int> num(0, n - 1); // 0-n-1的随机数
std::vector<int> position;
int cnt = 1 << 26;
for (int i = 0; i < cnt; i++){
position.push_back(num(gen));
}
int sum = 0;
std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < cnt; i++){
sum += arr[position[i]]; // 随机访问
}
std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();

std::chrono::duration<double> t = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);

double total = t.count();
std::cout << "size = " << (size >> 10) << "KB, time = " << total << "s" << std::endl;

delete[] arr;
}

void test_cache_line(){
unsigned int size = (1 << 26);
int n = size / sizeof(char);
char *arr = new char[n];
memset(arr, 1, n * sizeof(char));

for (auto s : strides){
int sum = 0;

std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
for(int i = 0; i < s; i++){
for(int j = 0; j < n; j += s){
sum += arr[j];
}
}
std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> t = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
double total = t.count();
std::cout << "stride = " << s << "Byte, time = " << total << "s" << std::endl;
}
delete[] arr;
}
int main(){
for (auto s : sizes){
test_cache(s * 1024);
}
test_cache_line();
return 0;
}

image-20240614145125146

image-20240614145134331

对于缓存大小,很容易发现,在128kb到384kb之间时间发生断层,1024kb到2048kb之间的时间差距相比其他也较大,4096kb到5120kb之间发生断层,之后的时间差距普遍较大,推测是储存空间超出缓存后直接访问主存导致的。

对于cache行测试,发现步长大于32byte后变化较大,推测L1的cacheline为32B

image-20240614145151076

发现L3缓存的大小和计算结果有较大出入,换了别人的新电脑测试代码后无较大出入,猜测是本人电脑L3缓存硬件老化或者其他原因导致的。

心得体会

通过本次实验,我们加深了对Cache工作原理的理解,体验了程序中访存模式变化对Cache效率和程序性能的影响。同时,通过实际的测试和分析,我们获得了关于X86机器上Cache层次结构和容量的信息。这些实验结果对于优化程序性能和了解计算机体系结构有着重要的指导意义。

通过实验,我们得出了以下结论和分析:

  1. 分析Cache访存模式对系统性能的影响:

通过对矩阵乘法算法的优化,我们发现访存模式对系统性能有显著影响。通过改变矩阵大小,我们记录了不同大小下的执行时间,并计算了加速比。优化后的算法在大部分情况下都显著提升了性能,加速比较高,说明优化后的访存模式能够更好地利用缓存,提高缓存命中率,从而提升程序性能。

  1. 测量分析出Cache的层次结构、容量以及L1 Cache行的大小:

通过设计方案并编写代码,我们成功测量了X86机器上的Cache层次结构和容量,并推断出L1 Cache行的大小。通过测量不同访问步长下的内存访问速度变化,我们观察到了缓存的局部性和缓存行的影响。根据实验数据,我们推测出L1 Cache行的大小约为32-64字节,与经验结果相吻合。