前言
今天做了实验5,通过 c 语言测 cache 参数。其实按理来说挺简单的,只是我们班没给代码,纯靠自己。听说别的班有给代码
示例代码也有,在《深入理解计算机系统》这本书的 6.6 小结。
上面给的代码是 Linux 平台下的,而且需要一个 fcyc2 头文件,可以在 这里 找到。出于某些原因 我是懒狗 我并没有用这份代码,我想在 win10 下进行测试。
⚠
我没有在 Linux 下试过课本上面的代码。。。
我是在 win10 下测试的。。。
所以 。。。
我并不确定我的代码是否正确。。。
请谨慎食用 Orz
注意事项(⚠ 重要)
唔。。。。我再编辑下,因为这个实验在 win10 下不是特别容易成功,有很多值得注意的地方:
- 请不要使用 visual studio 这个 IDE,因为它是大聪明,会优化掉你的代码。
- 最好使用 dev c++ 这个 IDE,并且开启 std c++11 才能完整地运行代码
开启方法:
- 最好增大测试的次数,114514190 就是一个好数字!
- 不要使用 rand() 这些小随机数生成器
- 不要在最内层循环直接生成随机数,因为生成随机数时间远大于访问内存,这样最多只能测出 L3 的大小
别问我怎么知道的 - 运行时最好关闭所有应用程序,什么秋秋,微信,网抑云,
关掉统统关掉!防止 cpu 抢占 - 可以通过 CPU-Z 这个软件查询精确的缓存大小,但是 emmm 因为我的代码不能够精确地测出 L1 数据缓存和 cache Line 的大小,于是我就用任务管理器糊弄一下就交了报告。。。
- 实验的几个参数最好起不同的变量名。比如随机数的范围就是数组大小,而访问次数应该被设定为一个常数(比如 114514190)
- 以虔诚地跪拜姿势,点击启动运行按钮
- 我编不下去了 Orz
总之就是很玄学 祝大家实验顺利。。。 我先 run 了(逃
实验说明
增进对cache工作原理的理解
体验程序中访存模式变化是如何影响 cahce 效率进而影响程序性能的过程;学习在 X86 真实机器上通过调整程序访存模式来探测多级 cache 结构以及 TLB 的大小。
按照下面的实验步骤及说明,完成相关操作记录实验过程的截图:
- x86 cache 层次结构的测量:
首先设计一个用于测量 x86 系统上 cache 层次结构的方案然后设计出相应的代码;然后,运行你的代码获得相应的测试数据。最后,根据测试数据分析你的x86机器有几级cache,各自容量是多大。 - 选做:尝试测量你 L1 cache 行的大小
- 选做:尝试测量你的 x86 机器 TLB 有多大
要求 1(90分)(报告撰写质量10分)
实验步骤
cache 层级的测量
首先测量我们的电脑 cache 的层级关系。我的方案是这样的:
- 开辟一块大小为 size kb 的内存空间
- 进行若干次随机内存访问
- 记录时间,计算平均的数据吞吐量(kb/s)
- 画图分析,记录 size 与 kb/s 的关系
注:
随机数生成,不要用 rand() 口牙,rand() 范围为 0~32768 好像,这才 30k 不到
我们随机访问是要产生大量随机且有意义的内存访问,要完全覆盖内存,这样才能使得内存尽可能被装进 cache 里面,这意味着随机数的范围需要非常大
要用 c++11 的 uniform_int_distribution
详见下文代码部分~
可行性分析:因为我们使用完全随机的地址进行内存访问,那么:
- 如果该内存能够被完整装入 cache,我们只需要花费很少的时间就可以访问到
- 如果内存块大小 size 超过我们的 cache 大小,即当前内存块不能被完整的放入 cache,那么我们的随机访问时间就会大大增加,因为发生了 miss
所以根据以上的两个分析,我们通过观察【吞吐量/数据集】大小的图表,就能分析出对应的 cahce 的大小和层次结构。
我使用 std c++ 11 在 Windows10 系统上进行实验,IDE 使用 dev c++,未开启任何 O1,O2 优化。
我们编写一个函数名叫 random_access,他会根据参数 size 的大小,创建一块大小为 size byte 的内存空间,并且进行 114514190 次随机访问,并且输出吞吐量(单位为 kb/s)。
void random_access(int size)
{
int n = size / sizeof(char);
char* buffer = new char[n];
fill(buffer, buffer+n, 1);
uniform_int_distribution<> dis(0, n-1);
int test_times = 11451419 * 10;
vector<int> random_index;
for(int i=0; i<test_times; i++)
{
int index = dis(gen);
random_index.push_back(index);
}
int sum = 0;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for(int i=0; i<test_times; i++)
{
sum += buffer[random_index[i]];
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
double dt = time_span.count();
cout<<(size/1024)<<" "<<(((double)sum/1024.0) / dt)<<endl;
delete[] buffer;
}
然后我们穷举 size,并且疯狂地调用 random_access2 进行测试即可:
void test1()
{
int size = 64 * KB;
vector<int> sizes{
8*KB,16*KB,32*KB,64*KB,128*KB,192*KB,256*KB,384*KB,512*KB,1024*KB,1536*KB,2048*KB,3072*KB,4096*KB,6144*KB,8192*KB};
for(auto s : sizes)
{
random_access(s);
}
}
随后我们将程序输出的数据导入到 excel 表格:
可以看到关系曲线分为三个阶段:
- 当 size 为 8-384kb 时,对应 L1 cache
- 当 size 为 384-3072 时,对应 L2 cache
- 当 size 为 3072-8192 时,对应 L3 cache
因为当 size 超出某一级 cache 的大小时,miss 增加,吞吐量会减少。观察图表我们可以得知,第一级 cache 大小大概在 256-384 这个范围。第二级 cache 大小大概为 2048-3072kb 左右。第三级 cache 大小大概在 3072-4096kb 左右。
我们查看任务管理器,发现我们的估计结果和实际结果基本吻合:
L1 cache line 测量
再来测量 L1 cache line 的大小。因为我们已经知道 L1 cache 的大小为 384kb,那么我们可以拟定如下的方案:
- 开辟一块内存
- 按照不同的步长 stride 进行若干次内存访问
- 记录时间,计算平均的数据吞吐量(kb/s)
- 画图分析,记录 stride 与 kbps 的关系
可行性分析:因为按照不同的步长,当我们的步长在 L1 cache line 之内,我们能够命中上一次访问数据时,载入 L1 cache 的数据行,而当我们的步长超出 L1 cache line 的大小,就会发生 miss,会拉低访问的时间。
于是我们编写一个函数名叫 stride_access,它根据传入的 stride,按照步长 stride,进行若干次顺序地内存访问,并且计算吞吐量。
void stride_access(char* buffer, int stride, int size)
{
int n = size / sizeof(char);
int sum = 0;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for(int j=0; j<stride; j++)
{
for(int i=0; i<n; i+=stride)
{
sum += buffer[i];
}
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
double dt = time_span.count();
cout<<stride<<" "<<(((double)sum/1024.0) / dt)<<endl;
}
然后我们穷举步长stride,并且重复调用stride_access进行测试:
void test2()
{
int size = 400 * MB;
int n = size / sizeof(char);
char* buffer = new char[n];
fill(buffer, buffer+n, 1);
vector<int> strides{
1*B,2*B,4*B,8*B,16*B,32*B,64*B,96*B,128*B,192*B,256*B,512*B,1024*B,1536*B,2048*B};
for(auto s : strides)
{
stride_access(buffer, s, size);
}
}
随后我们将输出的数据导入excel图表:
可以看到:
- 在步长 stride 位于 1-32 byte 之间时,吞吐量几乎不变
- 在步长 stride 位于 32-128 byte 之间,吞吐量逐步下降
- 在步长 stride 大于 128 byte 之后,吞吐量几乎不变
当步长 stride 小于 L1 cache line 时,若干次访问才会发生一次 miss(访问的偏移量超出一行 L1 cache line),而当步长 stride 大于 L1 cache line,每次访问都会miss!
根据测试数据,推测 L1 cache line 约为 32-64 b。而经验表明一般 cpu 都拥有 64b 或者 128b 的 cache line 大小,这与我们的测试结果相吻合。
实验总结
1. 注意不要使用编译器的任何编译优化
2. 使用 std c++ 的 random_device 进行高精度计时
3. 测试时尽量关闭其他应用程序,防止缓存的抢占
4. 要使用尽量大的测试次数,以保证测试结果不具有随机性
5. 不能在循环中生成随机数,应该事先生成好!因为生成耗时远大于访问内存
完整代码
#include <bits/stdc++.h>
#define B 1
#define KB 1024
#define MB 1048576
using namespace std;
using std::chrono::high_resolution_clock;
using std::chrono::duration;
using std::chrono::duration_cast;
random_device rd;//随机数生成
mt19937 gen(rd());
void random_access(int size)
{
int n = size / sizeof(char);
char* buffer = new char[n];
fill(buffer, buffer+n, 1);
uniform_int_distribution<> dis(0, n-1);
int test_times = 11451419 * 10;
vector<int> random_index;
for(int i=0; i<test_times; i++)
{
int index = dis(gen);
random_index.push_back(index);
}
int sum = 0;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for(int i=0; i<test_times; i++)
{
sum += buffer[random_index[i]];
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
double dt = time_span.count();
cout<<(size/1024)<<" "<<(((double)sum/1024.0) / dt)<<endl;
delete[] buffer;
}
void test1()
{
int size = 64 * KB;
vector<int> sizes{
8*KB,16*KB,32*KB,64*KB,128*KB,192*KB,256*KB,384*KB,512*KB,1024*KB,1536*KB,2048*KB,3072*KB,4096*KB,6144*KB,8192*KB};
for(auto s : sizes)
{
random_access(s);
}
}
void stride_access(char* buffer, int stride, int size)
{
int n = size / sizeof(char);
int sum = 0;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for(int j=0; j<stride; j++)
{
for(int i=0; i<n; i+=stride)
{
sum += buffer[i];
}
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
double dt = time_span.count();
cout<<stride<<" "<<(((double)sum/1024.0) / dt)<<endl;
}
void test2()
{
int size = 400 * MB;
int n = size / sizeof(char);
char* buffer = new char[n];
fill(buffer, buffer+n, 1);
vector<int> strides{
1*B,2*B,4*B,8*B,16*B,32*B,64*B,96*B,128*B,192*B,256*B,512*B,1024*B,1536*B,2048*B};
for(auto s : strides)
{
stride_access(buffer, s, size);
}
}
int main()
{
//test1();
test2();
return 0;
}