本次实验是CSAPP的第5个实验,此次实验主要是让咱们熟悉如何优化程序,如何写出更具备效率的代码。经过此次实验,咱们能够更好的理解计算机的工做原理,在之后编写代码时,具备能结合软硬件思考的能力。c++
@算法
本次实验主要处理优化内存密集型代码。图像处理提供了许多能够从优化中受益的功能示例。在本实验中,咱们将考虑两种图像处理操做:旋转,可将图像逆时针旋转90o,平滑,能够“平滑”或“模糊”图片。数组
在本实验中,咱们将考虑将图像表示为二维矩阵M,其中\({M_{i,j}}\)表示M的第(i,j)个像素的值,像素值是红色,绿色和蓝色(RGB)值的三倍。咱们只会考虑方形图像。令N表示图像的行(或列)数。行和列以C样式编号,从0到N − 1。缓存
给定这种表示形式,旋转操做能够很是简单地实现为如下两个矩阵运算:bash
转置:对于每对(i,j),\({M_{i,j}}\)和\({M_{j,i}}\)是互换的数据结构
交换行:第i行与第N-1 − i行交换。函数
具体以下图所示工具
经过用周围全部像素的平均值替换每一个像素值(在以该像素为中心的最大3×3窗口)中替换每一个像素值来实现平滑操做。以下图所示。像素的值\(M2[1][1]\) 和\(M2[N - 1][N - 1]\)以下所示:性能
\(M2[1][1] = \frac{{\sum\nolimits_{i = 0}^2 {\sum\nolimits_{j = 0}^2 {M1[i][j]} } }}{9}\)测试
\(M2[N - 1][N - 1] = \frac{{\sum\nolimits_{i = N - 2}^{N - 1} {\sum\nolimits_{j = N - 2}^{N - 1} {M1[i][j]} } }}{4}\)
本次实验中,咱们须要修改惟一文件是kernels.c。driver.c程序是一个驱动程序,可以让对咱们修改的程序进行评分。使用命令make driver生成驱动程序代码并使用./driver命令运行它。
图像的核心数据是用结构体表示的。像素是一个结构,以下所示:
typedef struct { unsigned short red; /* R value */ unsigned short green; /* G value */ unsigned short blue; /* B value */ } pixel;
能够看出,RGB值具备16位表示形式(“ 16位颜色”)。图像I表示为一维像素阵列,其中第(i,j)个像素为I [RIDX(i,j,n)]。这里n是图像矩阵的维数, RIDX是定义以下的宏:
#define RIDX(i,j,n) ((i)*(n)+(j))
有关此代码,请参见文件defs.h。
如下C函数计算将源图像src旋转90°的结果,并将结果存储在目标图像dst中。dim是图像的尺寸。
void naive_rotate(int dim, pixel *src, pixel *dst) { 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)]; return; }
上面的代码扫描源图像矩阵的行,而后复制到目标图像矩阵的列中。咱们的任务是使用代码移动,循环展开和阻塞等技术重写此代码,以使其尽量快地运行。(有关此代码,请参见文件kernels.c。)
平滑功能将源图像src做为输入,并在目标图像dst中返回平滑结果。这是实现的一部分:
void naive_smooth(int dim, pixel *src, pixel *dst) { int i, j; for(i=0; i < dim; i++) for(j=0; j < dim; j++) dst[RIDX(i,j,dim)] = avg(dim, i, j, src); /* Smooth the (i,j)th pixel */ return; }
函数avg返回第(i,j)个像素周围全部像素的平均值。咱们的任务是优化平滑(和avg)以尽量快地运行。 (注意:函数avg是一个局部函数,能够彻底摆脱它而以其余方式实现平滑)。(这段代码(以及avg的实现)位于kernels.c文件中。)
咱们的主要性能指标是CPE。若是某个函数须要C个周期来运行大小为N×N的图像,则CPE值为\(C/{N^2}\)。
咱们能够编写旋转和平滑例程的许多版本。为了帮助您比较编写的全部不一样版本的性能,咱们提供了一种“注册”功能的方式。
例如,咱们提供给您的文件kernels.c包含如下功能:
void register_rotate_functions() { add_rotate_function(&rotate, rotate_descr); }
此函数包含一个或多个调用以添加旋转函数。在上面的示例中,添加旋转函数将函数旋转与字符串旋转说明一块儿注册,该字符串是函数功能的ASCII描述。请参阅文件kernels.c以了解如何建立字符串描述。该字符串的长度最多为256个字符。
将编写的源代码将与咱们提供给驱动程序二进制文件的目标代码连接。要建立此二进制文件,您将须要执行如下命令
unix> make driver
每次更改kernels.c中的代码时,都须要从新制做驱动程序。要测试您的实现,而后能够运行如下命令:
unix> ./driver
该驱动程序能够在四种不一样的模式下运行:
默认模式,在其中运行实施的全部版本。
Autograder模式,其中仅运行rotation()和smooth()函数。这是当咱们使用驱动程序对您的切纸进行评分时将运行的模式。
文件模式,其中仅运行输入文件中提到的版本。
转储模式,其中每一个版本的单行描述转储到文本文件中。而后,您能够编辑该文本文件,以仅使用文件模式保留要测试的版本。您能够指定是在转储文件以后退出仍是要运行您的实现。
若是不带任何参数运行,驱动程序将运行全部版本(默认模式)。其余模式和选项能够经过驱动程序的命令行参数来指定,以下所示:
-g:仅运行rotate()和smooth()函数(自动分级模式)。
-f
-d
-q :将版本名称转储到转储文件后退出。与-d一块儿使用。例如,要在打印转储文件后当即退出,请键入./driver -qd dumpfile。
-h:打印命令行用法。
emsp; 回顾下经常使用的优化程序的方法,总结以下:
(1)高级设计
为遇到的问题选择适当的算法和数据结构。要特别警觉,避免使用那些会渐进地产生糟糕性能的算法或编码技术。
(2)基本编码原则
避免限制优化的因素,这样编译器就能产生高效的代码。
消除连续的函数调用。在可能时,将计算移到循环外。考虑有选择地妥协程序的模块性以得到更大的效率。
消除没必要要的内存引用。引入临时变量来保存中间结果。只有在最后的值计算出来时,才将结果存放到数组或全局变量中。
(3)低级优化
结构化代码以利用硬件功能。
展开循环,下降开销,而且使得进一步的优化成为可能。
经过使用例如多个累积变量和从新结合等技术,找到方法提升指令级并行。
用功能性的风格重写条件操做,使得编译采用条件数据传送。
(4)使用性能分析工具
当处理大型程序时,将注意力集中在最耗时的部分变得很重要。代码剖析程序和相关的工具能帮助咱们系统地评价和改进程序性能。咱们描述了 GPROF,一个标准的Unix剖析工具。还有更加复杂完善的剖析程序可用,例如 Intel的VTUNE程序开发系统,还有 Linux系统基本上都有的 VALGRIND。这些工具能够在过程级分解执行时间,估计程序每一个基本块( basic block)的性能。(基本块是内部没有控制转移的指令序列,所以基本块老是整个被执行的。)
在这一部分中,咱们将优化旋转以实现尽量低的CPE。您应该编译驱动程序,而后使用适当的参数运行它以测试您的实现。例如,运行提供的原始版本(用于旋转)的驱动程序将生成以下所示的输出:
函数源码以下:
void naive_rotate(int dim, pixel *src, pixel *dst) { 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)]; return; }
其中,defs.h中RIDX定义为:#define RIDX(i,j,n) ((i)*(n)+(j))下面详细分析下程序。
i = 0 j = 0 dest[20] = src[0] i = 1 j = 0 dest[21] = src[5] i = 0 j = 1 dest[15] = src[1] i = 1 j = 1 dest[16] = src[6] i = 0 j = 2 dest[10] = src[2] i = 1 j = 2 dest[11] = src[7] i = 0 j = 3 dest[5] = src[3] i = 1 j = 3 dest[6] = src[8] i = 0 j = 4 dest[0] = src[4] i = 1 j = 4 dest[1] = src[9]
具体以下图所示:
这段代码的做用就是将dim * dim大小的方块中全部的像素进行行列调位、致使整幅图画进行了90度旋转。观察源代码咱们发现,程序进行了嵌套循环,随着dim的增长,循环的复杂度愈来愈大,并且每循环一次,dim-1-j就要计算一次,所以,咱们考虑进行分块优化。
对于循环分块,这里的分块指的是一个应用级的数据组块,而不是高速缓存中的块,这样构造程序,能将一个片加载到L1高速缓存中去,并在这个片中进行所须要的全部读和写,而后丢掉这个片,加载下一个片,以此类推。
/*分块:8 * 8*/ char rotate_descr[] = "rotate1: Current working version"; void rotate(int dim, pixel *src, pixel *dst) { int i,j,i1,j1; for(i=0; i < dim; i+=8) for(j=0; j < dim; j+=8) for(i1=i; i1 < i+8; i1++) for(j1=j; j1 < j+8; j1++) dst[RIDX(dim-1-j1,i1,dim)] = src[RIDX(i1,j1,dim)]; }
优化后的版本测试以下所示:
右上图能够看到,得分有了明显的提高,Dim规模较小时,提高并不明显,在Dim为1024*1024时,由原来的17.1下降到了6.0.说明咱们的方法仍是有效的,可是最后的总得得分只有9.3分,效果不是很好。
char rotate_descr[] = "rotate2: Current working version"; void rotate(int dim, pixel *src, pixel *dst) { int i,j,i1,j1; for(i=0; i < dim; i+=32) for(j=0; j < dim; j+=32) for(i1=i; i1 < i+32; i1++) for(j1=j; j1 < j+32; j1++) dst[RIDX(dim-1-j1,i1,dim)] = src[RIDX(i1,j1,dim)]; }
本次继续采用的是分块策略,分为了32块,可是由下图的得分能够看到,性能基本有提高,因此,须要换个思路了。
在版本二的基础上,咱们进行循环展开,32路并行,并使用指针代替RIDX进行数组访问,这里牺牲了程序的尺寸来换取速度优化。
char rotate_descr[] = "rotate3: Current working version"; void rotate(int dim, pixel *src, pixel *dst) { int i,j; int dst_base = (dim-1)*dim; dst +=dst_base; for(i = 0;i < dim;i += 32){ for(j = 0;j < dim;j++){ *dst = *src; src +=dim; dst++; //31组 *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src++; src -= (dim<<5)-dim; //src -=31*dim; dst -=31+dim; } dst +=dst_base + dim; dst +=32; src +=(dim<<5)-dim; //src +=31*dim; } }
咱们在版本二的基础上,将原来的程序的内循环展开成32个并行,每一次同时处理32个像素点,即将循环次数减小了32倍,大大加速了程序。分数由版本二的10.3分涨到了15.3分。特别是在哦1024 * 1024时,由15.1直接降到了5.0,性能提高仍是很明显的。
在这一部分中,您将优化平滑度以实现尽量低的CPE。例如,运行提供的朴素版本(为了平滑)的驱动程序将生成以下所示的输出:
unix> ./driver Smooth: Version = naive_smooth: Naive baseline implementation: Dim 32 64 128 256 512 Mean Your CPEs 695.8 698.5 703.8 720.3 722.7 Baseline CPEs 695.0 698.0 702.0 717.0 722.0 Speedup 1.0 1.0 1.0 1.0 1.0 1.0
void naive_smooth(int dim, pixel *src, pixel *dst) { int i, j; for(i=0; i < dim; i++) for(j=0; j < dim; j++) dst[RIDX(i,j,dim)] = avg(dim, i, j, src); /* Smooth the (i,j)th pixel */ return; }
这个函数的做用是平滑图像,在smooth函数中由于要求周围点的平均值,因此会频繁的调用avg函数,并且avg函数仍是一个2层for循环,因此咱们能够考虑循环展开或者消除函数调用等方法,减小avg函数调用和循环。
Smooth函数处理分为4块,一为主体内部,由9点求平均值;二为4个顶点,由4点求平均值;三为四条边界,由6点求平均值。从图片的顶部开始处理,再上边界,顺序处理下来,其中在处理左边界时,for循环处理一行主体部分。
未经优化的函数性能以下,得分为12.2分。
void smooth(int dim, pixel *src, pixel *dst) { pixel_sum rowsum[530][530]; int i, j, snum; for(i=0; i<dim; i++) { rowsum[i][0].red = (src[RIDX(i, 0, dim)].red+src[RIDX(i, 1, dim)].red); rowsum[i][0].blue = (src[RIDX(i, 0, dim)].blue+src[RIDX(i, 1, dim)].blue); rowsum[i][0].green = (src[RIDX(i, 0, dim)].green+src[RIDX(i, 1, dim)].green); rowsum[i][0].num = 2; for(j=1; j<dim-1; j++) { rowsum[i][j].red = (src[RIDX(i, j-1, dim)].red+src[RIDX(i, j, dim)].red+src[RIDX(i, j+1, dim)].red); rowsum[i][j].blue = (src[RIDX(i, j-1, dim)].blue+src[RIDX(i, j, dim)].blue+src[RIDX(i, j+1, dim)].blue); rowsum[i][j].green = (src[RIDX(i, j-1, dim)].green+src[RIDX(i, j, dim)].green+src[RIDX(i, j+1, dim)].green); rowsum[i][j].num = 3; } rowsum[i][dim-1].red = (src[RIDX(i, dim-2, dim)].red+src[RIDX(i, dim-1, dim)].red); rowsum[i][dim-1].blue = (src[RIDX(i, dim-2, dim)].blue+src[RIDX(i, dim-1, dim)].blue); rowsum[i][dim-1].green = (src[RIDX(i, dim-2, dim)].green+src[RIDX(i, dim-1, dim)].green); rowsum[i][dim-1].num = 2; } for(j=0; j<dim; j++) { snum = rowsum[0][j].num+rowsum[1][j].num; dst[RIDX(0, j, dim)].red = (unsigned short)((rowsum[0][j].red+rowsum[1][j].red)/snum); dst[RIDX(0, j, dim)].blue = (unsigned short)((rowsum[0][j].blue+rowsum[1][j].blue)/snum); dst[RIDX(0, j, dim)].green = (unsigned short)((rowsum[0][j].green+rowsum[1][j].green)/snum); for(i=1; i<dim-1; i++) { snum = rowsum[i-1][j].num+rowsum[i][j].num+rowsum[i+1][j].num; dst[RIDX(i, j, dim)].red = (unsigned short)((rowsum[i-1][j].red+rowsum[i][j].red+rowsum[i+1][j].red)/snum); dst[RIDX(i, j, dim)].blue = (unsigned short)((rowsum[i-1][j].blue+rowsum[i][j].blue+rowsum[i+1][j].blue)/snum); dst[RIDX(i, j, dim)].green = (unsigned short)((rowsum[i-1][j].green+rowsum[i][j].green+rowsum[i+1][j].green)/snum); } snum = rowsum[dim-1][j].num+rowsum[dim-2][j].num; dst[RIDX(dim-1, j, dim)].red = (unsigned short)((rowsum[dim-2][j].red+rowsum[dim-1][j].red)/snum); dst[RIDX(dim-1, j, dim)].blue = (unsigned short)((rowsum[dim-2][j].blue+rowsum[dim-1][j].blue)/snum); dst[RIDX(dim-1, j, dim)].green = (unsigned short)((rowsum[dim-2][j].green+rowsum[dim-1][j].green)/snum); } }
在以上的优化中,咱们取消了对avg函数的直接调用,而是直接对像素点的fgb颜色分别求均值,而且将重复利用的数据存储在了数组之中,所以,速度比以前有所提高,可是提高并不高,由12.2提高到15.4。
void smooth(int dim, pixel *src, pixel *dst) { int i,j; int dim0=dim; int dim1=dim-1; int dim2=dim-2; pixel *P1, *P2, *P3; pixel *dst1; P1=src; P2=P1+dim0; //左上角像素处理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red)>>2; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green)>>2; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue)>>2; dst++; //上边界处理 for(i=1;i<dim1;i++) { dst->red=(P1->red+(P1+1)->red+(P1+2)->red+P2->red+(P2+1)->red+(P2+2)->red)/6; dst->green=(P1->green+(P1+1)->green+(P1+2)->green+P2->green+(P2+1)->green+(P2+2)->green)/6; dst->blue=(P1->blue+(P1+1)->blue+(P1+2)->blue+P2->blue+(P2+1)->blue+(P2+2)->blue)/6; dst++; P1++; P2++; } //右上角像素处理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red)>>2; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green)>>2; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue)>>2; dst++; P1=src; P2=P1+dim0; P3=P2+dim0; //左边界处理 for(i=1;i<dim1;i++) { dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red+P3->red+(P3+1)->red)/6; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green+P3->green+(P3+ 1)->green)/6; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue+P3->blue+(P3+1)->blue)/6; dst++; dst1=dst+1; //中间主体部分处理 for(j=1;j<dim2;j+=2) { //同时处理两个像素 dst->red=(P1->red+(P1+1)->red+(P1+2)->red+P2->red+(P2+1)->red+(P2+2)->red+P3->red+(P3+1)->red+(P3+2)->red)/9; dst->green=(P1->green+(P1+1)->green+(P1+2)->green+P2->green+(P2+1)->green+(P2+2)->green+P3->green+(P3+1)->green+(P3+2)->green)/9; dst->blue=(P1->blue+(P1+1)->blue+(P1+2)->blue+P2->blue+(P2+1)->blue+(P2+2)->blue+P3->blue+(P3+1)->blue+(P3+2)->blue)/9; dst1->red=((P1+3)->red+(P1+1)->red+(P1+2)->red+(P2+3)->red+(P2+1)->red+(P2+2)->red+(P3+3)->red+(P3+1)->red+(P3+2)->red)/9; dst1->green=((P1+3)->green+(P1+1)->green+(P1+2)->green+(P2+3)->green+(P2+1)->green+(P2+2)->green+(P3+3)->green+(P3+1)->green+(P3+2)->green)/9; dst1->blue=((P1+3)->blue+(P1+1)->blue+(P1+2)->blue+(P2+3)->blue+(P2+1)->blue+(P2+2)->blue+(P3+3)->blue+(P3+1)->blue+(P3+2)->blue)/9; dst+=2; dst1+=2; P1+=2; P2+=2; P3+=2; } for(;j<dim1;j++) { dst->red=(P1->red+(P1+1)->red+(P1+2)->red+P2->red+(P2+1)->red+(P2+2)->red+P3->red+(P3+1)->red+(P3+2)->red)/9; dst->green=(P1->green+(P1+1)->green+(P1+2)->green+P2->green+(P2+1)->green+(P2+2)->green+P3->green+(P3+1)->green+(P3+2)->green)/9; dst->blue=(P1->blue+(P1+1)->blue+(P1+2)->blue+P2->blue+(P2+1)->blue+(P2+2)->blue+P3->blue+(P3+1)->blue+(P3+2)->blue)/9; dst++; P1++; P2++; P3++; } //右侧边界处理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red+P3->red+(P3+1)->red)/6; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green+P3->green+(P3+1)->green)/6; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue+P3->blue+(P3+1)->blue)/6; dst++; P1+=2; P2+=2; P3+=2; } //右下角处理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red)>>2; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green)>>2; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue)>>2; dst++; //下边界处理 for(i=1;i<dim1;i++) { dst->red=(P1->red+(P1+1)->red+(P1+2)->red+P2->red+(P2+1)->red+(P2+2)->red)/6; dst->green=(P1->green+(P1+1)->green+(P1+2)->green+P2->green+(P2+1)->green+(P2+2)->green)/6; dst->blue=(P1->blue+(P1+1)->blue+(P1+2)->blue+P2->blue+(P2+1)->blue+(P2+2)->blue)/6; dst++; P1++; P2++; } //右下角像素处理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red)>>2; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green)>>2; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue)>>2; }
在这个版本中,咱们在版本一的基础上继续优化。将Smooth函数分为内部-顶点-边界的四部分,一为主体内部,由9点求平均值;二为4个顶点,由4点求平均值;三为四条边界,由6点求平均值。从图片的顶部开始处理,再上边界,顺序处理下来,其中在处理左边界时,for循环处理一行主体部分,就是以上的代码。
下图为测试结果,由版本一的分15.4分提高到了44.1分,性能提高显著!
本次实验的趣味性不如前几个实验,难度也没有前几个实验的大。在实际优化程序时,咱们不能一味的为了速度而展开程序,或者消除函数引用,以程序的体积和可读性去换取性能的提高是很是不划算的。在保证可读性的前提下尽量去提高程序的性能。
有任何问题,都可经过公告中的二维码联系我