深入理解计算机系统CSAPP-perfLab:kernels.c性能优化实验:rotate优化详细实验日志(含六个优化版本)

一、实验内容

1、 学习图像旋转与图像平滑功能的实现;
2、 理解Makefile的规则与make程序的工作原理;
3、 了解Cache写缺失与读缺失的基本原理,并尝试用Cache相关原理进行性能优化
4、 修改kernels.c文件并编译运行driver程序,尽可能优化程序性能。

二、相关知识

1、 图形逆时针旋转90°的实现

在图像的存储的基础上进行线性变换。首先将图像座标变换为数学座标,之后求矩阵转置,再执行行交换,即实现了图形逆时针旋转90°。
在这里插入图片描述我们将其用一维的形式表示:
可以看出,用常规的行访问方式实现旋转功能的方法,读操作的局部性较好,但写操作的空间局部性很差,这为性能优化提供了方向。

2、 服务器与本地计算机之间复制文件的方法

a) 把本地文件拷贝到服务器
scp 本地文件路径 用户名@服务器地址:/服务器路径

b) 把服务器文件拷贝到本地计算机
scp 用户名@服务器地址:/服务器路径 本地文件路径

3、 常见性能优化方法

代码移动、减少函数调用、减少访存次数、分支预测、循环展开、并行处理、cache友好等。

4、 Makefile规则

target …:prerequisites …
command 1
command 2

target:即目标文件,可以是Object File、执行文件、一个标签;
prerequisites:即要生成该target所需要(依赖)的文件或是目标;
command即make需要执行的命令,可以是任意的Shell命令。

注意,要执行的命令行一定要以一个Tab键作为开头。在Makefile中的定义的变量,就像是C/C++语言中的宏一样,代表了一个文本字串,Makefile中执行的时候其会自动原模原样地展开。变量在声明时需要给予初值,而在使用时,需要给在变量名前加上“$”符号,但最好用小括号“()”或是大括号“{}”。

5、 kernels.c中的结构体team
team_t team={
“R1701”,//班级
“SCRECT”//姓名
“2017001”,//学号
“”,
“”
};

6、 64位系统中RGB像素点(结构体)的存储
在这里插入图片描述
可以看到,每一个像素点占0.75个字长。
进一步分析可知,只有1/2的像素点存储在一个块中,读或写余下的像素点需要访问两个块。

7、 数组在内存中的存储原理

内存是一维的,因此不管是一维数组、二维数据、n维数组,在内存中都是以一维的方式存储的。二维数组是以行优先的规则在内存中存储的。

在这里插入图片描述

三、实验步骤

说明:由于我的老师是把实验文件直接放在了服务器里并且设置好了每位同学的用户名,这里我将整个perfLab文件夹复制到本地计算机,通过本地编译器编译,以便进一步研究优化方案。如果老师是通过课程中心、通知群、邮箱等下发的实验文件,直接下载文件之后修改kernels.c就好啦!

1、 进入终端。若为Windows操作系统,按下”Windows”+”R”,输入”cmd”进入;若为Linux操作系统,在首页选择”Terminal”。

2、 输入指令” ssh 域名-l用户名”,输入密码。登陆服务器。

3、 成功登陆之后,可以通过pwd命令与ls命令查看当前所在目录与当前目录下的文件。如果需要退出登录,输入exit。

4、 在登录状态下,cd进入perfLab文件夹内,执行perf_init命令进行初始化。
在这里插入图片描述

5、 从服务器中拷贝文件到本地:
通过”scp 本地文件路径 用户名@服务器地址:/服务器路径”将本地文件拷贝到服务器,也可以通过” scp 用户名@服务器地址:/服务器路径 本地文件路径”将服务器文件拷贝到本地。对于文件夹的拷贝,需要在scp后加”-r”参数。

这里我先将实验文件从服务器中拷贝到本地,修改完成之后再上传回服务器。也可以直接在服务器通过vim修改文件。

6、 回到服务器,在perfLab文件夹下执行make。
在这里插入图片描述

7、 输入“./driver”命令进行评分。
在这里插入图片描述

四、 程序优化各个版本

初始版本

在这里插入图片描述
对应代码:

int i, j;
for (i = 0; i < dim; i++)
for (j = 0; j < dim; j++)
dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)];

其他所有版本均与此版本作比较。可以看到Rotate的总分维持在5.0左右,与实验环境有关。

版本一:分块,旨在提高空间局部性

核心代码如下:

int i,j,ki,kj;
//分块,我这里分为8*8作为一块 
   for (i = 0; i < dim; i+=8) //每块8行
	for (j = 0; j < dim; j+=8) //每块8列
		for(ki=i; ki<i+8; ki++) 
			for(kj=j; kj<j+8; kj++) 
					dst[RIDX(dim-1-kj, ki, dim)] = src[RIDX(ki, kj, dim)];

运行结果为:
在这里插入图片描述
可以看到Rotate的Summary由5.0提高至7.9,Dim规模较小时CPE优化不明显,当Dim规模较大时可以看到CPE明显有所下降(如对于1024*1024,CPE由35.4下降到11.6)。但总体来说,这一优化方法有效但不高效,需要再寻找其他方法。

版本二:在分块的基础上,循环展开(降低了循环开销,但牺牲程序的尺寸)

核心代码如下:

int i,j,ki,kj;
//分块+循环展开,这里32*32分块,4*4循环展开
   for (i = 0; i < dim; i+=32) //每块32行
	for (j = 0; j < dim; j+=32) //每块32列
		for(ki=i; ki<i+32; ki+=4) //控制循环展开的行
			//控制循环展开的列
			for(kj=j; kj<j+32; kj+=4) {
				//相当于在4*4的小块内,手写每一个像素点的旋转变换
				dst[RIDX(dim-1-kj, ki, dim)] = src[RIDX(ki, kj, dim)];
				dst[RIDX(dim-1-kj-1, ki, dim)] = src[RIDX(ki, kj+1, dim)];
				dst[RIDX(dim-1-kj-2, ki, dim)] = src[RIDX(ki, kj+2, dim)];
				dst[RIDX(dim-1-kj-3, ki, dim)] = src[RIDX(ki, kj+3, dim)];
				dst[RIDX(dim-1-kj, ki+1, dim)] = src[RIDX(ki+1, kj, dim)];
				dst[RIDX(dim-1-kj-1, ki+1, dim)] = src[RIDX(ki+1, kj+1, dim)];
				dst[RIDX(dim-1-kj-2, ki+1, dim)] = src[RIDX(ki+1, kj+2, dim)];
				dst[RIDX(dim-1-kj-3, ki+1, dim)] = src[RIDX(ki+1, kj+3, dim)];
				dst[RIDX(dim-1-kj, ki+2, dim)] = src[RIDX(ki+2, kj, dim)];
				dst[RIDX(dim-1-kj-1, ki+2, dim)] = src[RIDX(ki+2, kj+1, dim)];
				dst[RIDX(dim-1-kj-2, ki+2, dim)] = src[RIDX(ki+2, kj+2, dim)];
				dst[RIDX(dim-1-kj-3, ki+2, dim)] = src[RIDX(ki+2, kj+3, dim)];
				dst[RIDX(dim-1-kj, ki+3, dim)] = src[RIDX(ki+3, kj, dim)];
				dst[RIDX(dim-1-kj-1, ki+3, dim)] = src[RIDX(ki+3, kj+1, dim)];
				dst[RIDX(dim-1-kj-2, ki+3, dim)] = src[RIDX(ki+3, kj+2, dim)];
				dst[RIDX(dim-1-kj-3, ki+3, dim)] = src[RIDX(ki+3, kj+3, dim)];		
			}

运行结果如图:
在这里插入图片描述
总体上性能优于原始版本与仅分块版本,Summary达到9.4。但仔细分析CPE的变化,发现在输入规模较小(64、128)时,使用循环展开后的性能反而不如不展开,但当输入规模较大时,性能提升很明显(如对于1024*1024,CPE由35.4下降到9.7)。

版本三:在前两个版本的基础上,改善读写顺序

具体来说,先处理矩阵第一列的前32个元素,再处理第二列前32个元素,以此类推直到处理完毕矩阵的前32行,再以相同的方法继续处理余下的矩阵元素。核心代码为:

int i,j;
//分块+循环展开+改变顺序
   for (i = 0; i < dim; i+=16)
	for (j = 0; j < dim; j++) {
			  	//在这里不再以行优先,而是每32行为一块,列优先
				dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)];
				dst[RIDX(dim-1-j, i+1, dim)] = src[RIDX(i+1, j, dim)];
				dst[RIDX(dim-1-j, i+2, dim)] = src[RIDX(i+2, j, dim)];
				dst[RIDX(dim-1-j, i+3, dim)] = src[RIDX(i+3, j, dim)];
				dst[RIDX(dim-1-j, i+4, dim)] = src[RIDX(i+4, j, dim)];
				dst[RIDX(dim-1-j, i+5, dim)] = src[RIDX(i+5, j, dim)];
				dst[RIDX(dim-1-j, i+6, dim)] = src[RIDX(i+6, j, dim)];
				dst[RIDX(dim-1-j, i+7, dim)] = src[RIDX(i+7, j, dim)];
				dst[RIDX(dim-1-j, i+8, dim)] = src[RIDX(i+8, j, dim)];
				dst[RIDX(dim-1-j, i+9, dim)] = src[RIDX(i+9, j, dim)];
				dst[RIDX(dim-1-j, i+10, dim)] = src[RIDX(i+10, j, dim)];
				dst[RIDX(dim-1-j, i+11, dim)] = src[RIDX(i+11, j, dim)];
				dst[RIDX(dim-1-j, i+12, dim)] = src[RIDX(i+12, j, dim)];
				dst[RIDX(dim-1-j, i+13, dim)] = src[RIDX(i+13, j, dim)];
				dst[RIDX(dim-1-j, i+14, dim)] = src[RIDX(i+14, j, dim)];
				dst[RIDX(dim-1-j, i+15, dim)] = src[RIDX(i+15, j, dim)];
}

运行结果如图:
在这里插入图片描述
总体上性能进一步提高,但仍存在不足。仔细分析CPE的变化,发现在输入规模较小时改善读写顺序可以大幅度提高程序性能,但对于输入规模为1024*1024这种情况下,版本三的CPE不如版本二的CPE。结合数组的存储原理,推测这是由于当图像规模较大时,同一列的相邻32个像素点的读写间隔着比较多的读写,导致局部性变差。

注:初始版本、版本一到版本三运行环境为公用1核2G服务器,
版本四到版本六运行环境为自用1核2G服务器,
自用服务器评分偏高,对初始版本的评分为6.5。

版本四:修改pixel结构,旨在令每一个像素点占1整个块

在这里插入图片描述
但考虑到复制每一个像素点需要访问更多的字节,因此取结构体的red、green、blue分别复制。除此之外,改善读写顺序。具体来说,先处理矩阵第一列的前32个元素,再处理第二列前32个元素,以此类推直到处理完毕矩阵的前32行,再以相同的方法继续处理余下的矩阵元素。核心代码为:

for (i = 0; i < dim; i+=32)
	for (j = 0; j < dim; j++)
		for(k=0; k<32; k++) {
			dst[RIDX(dim-1-j, i+k, dim)].red = src[RIDX(i+k, j, dim)].red;
			dst[RIDX(dim-1-j, i+k, dim)].green = src[RIDX(i+k, j, dim)].green;
			dst[RIDX(dim-1-j, i+k, dim)].blue = src[RIDX(i+k, j, dim)].blue;
	}

运行结果为:
在这里插入图片描述
可以看到Rotate的Summary由6.5提高至12.9,但Dim规模较小时CPE优化不明显,尤其是当Dim=64时该版本对应的CPE不降反增。另一方面,当Dim规模较大时可以看到CPE明显有所下降,Dim=512对应的加速比更是达到了20.8。当Dim继续增大时,加速比降低,由此推测该版本优化在Dim=512时的优化效果最为明显。但分析原理,虽然对于每个像素点都只需要访问一个块,但这样处理无疑增加了块的总数,推测这是制约程序性能进一步提高的因素。

版本五:消除函数调用并改善读写顺序。

说明:考虑到程序过多次调用RIDX函数,故消除该函数的调用。此外,改善读写顺序。具体来说,先处理矩阵第一列的前32个元素,再处理第二列前32个元素,以此类推直到处理完毕矩阵的前32行,再以相同的方法继续处理余下的矩阵元素。核心代码为:

int i,j,k;
for (i = 0; i < dim; i+=32)
	for (j = 0; j < dim; j++)
		for(k=0; k<32; k++) {
		//dst[RIDX(dim-1-j, i+k, dim)] = src[RIDX(i+k, j, dim)];
		dst[(dim-1-j)*dim+i+k] = src[(i+k)*dim+j];
		}

运行结果如图:
在这里插入图片描述
总体上性能优于其他所有版本,推测cache块结构为32。在fcyc.c中查看到” #define CACHE_BLOCK 32”,证明推测正确。该版本Summary达到16.1。与版本四相比,当Dim<1024时性能均有明显提升,但当Dim=1024时加速比出现明显下降。

版本六:改善读写顺序并循环站靠。

说明:改善读写顺序的具体方案与版本五相同。循环展开,并使用指针代替RIDX进行数组访问。核心代码为:

int i, j;
//src初始化为第一行第一个像素点,dst与之对应,应指向最后一行第一个像素点
dst += (dim-1)*dim;
for (i = 0; i < dim; i+=32){ 
	for (j = 0; j < dim; j++){ 
		//循环展开,共32:
		*dst=*src; src+=dim; dst+=1;
        *dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src;
		//注意修改src与dst指向的位置,改为下一列的相应32行
		src++;
		src-= (dim<<5)-dim; 
		dst-=31+dim;
	}
	dst+=dim*dim;
	dst+=32;
	src += (dim<<5)-dim;
}

运行结果如图:
在这里插入图片描述
总体上性能不如版本五,对每个dim的加速比同样不如版本五。分析加速比变化趋势,发现当输入图像规模不断增大,加速比先升后降,在Dim=512时取得最大值25.1。总的来说,Summary在14~16之间浮动,表明使用指针+循环展开+改善读写顺序的程序性能受输入图像的影响较大。

五、遇到的问题及解决方法

1、 未进入perfLab文件夹导致无法make
在这里插入图片描述
解决方法:导致该问题的原因是没有在指定的文件夹下执行make,解决方法为cd进入perfLab文件夹,再执行make。
在这里插入图片描述
2、 选择了不合适的分块大小且未加入边界检查

选择18x18作为块大小,运行时出现错误。执行评分时可以清楚地看到,第一个测试用例对应的Dim规模为32x32,因此在未进行边界检查的情况下,会发生数组访问越界,无法得到预期结果。加入边界检查,由于if判断所带来的时间开销,导致程序性能明显下降。综合考虑,放弃了这种无法整分的分块方法。

3、 代码出现细节上的错误

在程序修改阶段未仔细检查,评分时出现:
在这里插入图片描述
检查代码,原来是实现分块时,内循环与外循环的循环控制变量混淆了。这也提醒了我,在修改代码时一定要认真仔细,有了这一次出错的经验,在后面实现循环展开时没有再出现同样的错误。

写在最后:
如果觉得这篇博客帮到了你,请给博主点一个大大的赞!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章