计组实验5:cache大小测量与 cache line 大小测量

前言

今天做了实验5,通过 c 语言测 cache 参数。其实按理来说挺简单的,只是我们班没给代码,纯靠自己。听说别的班有给代码

示例代码也有,在《深入理解计算机系统》这本书的 6.6 小结。

在这里插入图片描述

上面给的代码是 Linux 平台下的,而且需要一个 fcyc2 头文件,可以在 这里 找到。出于某些原因 我是懒狗 我并没有用这份代码,我想在 win10 下进行测试。



我没有在 Linux 下试过课本上面的代码。。。
我是在 win10 下测试的。。。
所以 。。。
我并不确定我的代码是否正确。。。
请谨慎食用 Orz

注意事项(⚠ 重要)

唔。。。。我再编辑下,因为这个实验在 win10 下不是特别容易成功,有很多值得注意的地方:

  1. 请不要使用 visual studio 这个 IDE,因为它是大聪明,会优化掉你的代码。
  2. 最好使用 dev c++ 这个 IDE,并且开启 std c++11 才能完整地运行代码

开启方法:
在这里插入图片描述

  1. 最好增大测试的次数,114514190 就是一个好数字!
  2. 不要使用 rand() 这些小随机数生成器
  3. 不要在最内层循环直接生成随机数,因为生成随机数时间远大于访问内存,这样最多只能测出 L3 的大小别问我怎么知道的
  4. 运行时最好关闭所有应用程序,什么秋秋,微信,网抑云,关掉统统关掉! 防止 cpu 抢占
  5. 可以通过 CPU-Z 这个软件查询精确的缓存大小,但是 emmm 因为我的代码不能够精确地测出 L1 数据缓存和 cache Line 的大小,于是我就用任务管理器糊弄一下就交了报告。。。
  1. 实验的几个参数最好起不同的变量名。比如随机数的范围就是数组大小,而访问次数应该被设定为一个常数(比如 114514190)
  2. 虔诚地跪拜姿势,点击启动运行按钮
  3. 我编不下去了 Orz

总之就是很玄学 祝大家实验顺利。。。 我先 run 了(逃

实验说明

增进对cache工作原理的理解

体验程序中访存模式变化是如何影响 cahce 效率进而影响程序性能的过程;学习在 X86 真实机器上通过调整程序访存模式来探测多级 cache 结构以及 TLB 的大小。

按照下面的实验步骤及说明,完成相关操作记录实验过程的截图:

  1. x86 cache 层次结构的测量:
    首先设计一个用于测量 x86 系统上 cache 层次结构的方案然后设计出相应的代码;然后,运行你的代码获得相应的测试数据。最后,根据测试数据分析你的x86机器有几级cache,各自容量是多大。
  2. 选做:尝试测量你 L1 cache 行的大小
  3. 选做:尝试测量你的 x86 机器 TLB 有多大

要求 1(90分)(报告撰写质量10分)

实验步骤

cache 层级的测量

首先测量我们的电脑 cache 的层级关系。我的方案是这样的:

  1. 开辟一块大小为 size kb 的内存空间
  2. 进行若干次随机内存访问
  3. 记录时间,计算平均的数据吞吐量(kb/s)
  4. 画图分析,记录 size 与 kb/s 的关系

注:
随机数生成,不要用 rand() 口牙,rand() 范围为 0~32768 好像,这才 30k 不到
我们随机访问是要产生大量随机且有意义的内存访问,要完全覆盖内存,这样才能使得内存尽可能被装进 cache 里面,这意味着随机数的范围需要非常大
要用 c++11 的 uniform_int_distribution
详见下文代码部分~

可行性分析:因为我们使用完全随机的地址进行内存访问,那么:

  1. 如果该内存能够被完整装入 cache,我们只需要花费很少的时间就可以访问到
  2. 如果内存块大小 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 表格:

在这里插入图片描述
可以看到关系曲线分为三个阶段:

  1. 当 size 为 8-384kb 时,对应 L1 cache
  2. 当 size 为 384-3072 时,对应 L2 cache
  3. 当 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,那么我们可以拟定如下的方案:

  1. 开辟一块内存
  2. 按照不同的步长 stride 进行若干次内存访问
  3. 记录时间,计算平均的数据吞吐量(kb/s)
  4. 画图分析,记录 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图表:

在这里插入图片描述
可以看到:

  1. 在步长 stride 位于 1-32 byte 之间时,吞吐量几乎不变
  2. 在步长 stride 位于 32-128 byte 之间,吞吐量逐步下降
  3. 在步长 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;
}

猜你喜欢

转载自blog.csdn.net/weixin_44176696/article/details/111873717
今日推荐