从矩阵乘法来看-O优化和ijk执行顺序对程序性能的影响

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lusongno1/article/details/82824381

从矩阵乘法来看-O优化和ijk执行顺序对程序性能的影响

根据计算矩阵乘积的c程序,主要想做想做两件事情:

  • 统计采用不同的优化选项编译程序所用的时间,感受-O优化带来的性能提升。

  • 看看矩阵乘法中不同循环顺序对程序性能的影响: 改变三重循环的顺序,统计不同循环顺序的运行时间、性能和效率。

处理器参数

先自报一下家门,老电脑了,四核处理器,每个核心2.60GHz,单核每个时钟周期执行的浮点运算次数大约为25。
= cpu 理论峰值速度=\text{cpu}主频*每个时钟周期执行的浮点运算次数*核数
我的笔记本的峰值计算能力为256亿浮点运算/秒,具体如下:

制造商 : Intel
型号 : Intel® Core™ i5-3230M CPU @ 2.60GHz
速度 : 3.2千兆赫(GHz)
最低/最高速度/涡轮速度 : 1.2千兆赫(GHz) - 2.6千兆赫(GHz) - 3.2千兆赫(GHz)
恒速 : 2.6千兆赫(GHz)
峰值处理性能 (PPP) : 25.6数十亿浮点运算/秒(GFLOPS)
调整后的峰值性能 (APP) : 7.68WG
内核/处理器 : 2 个
线程/内核 : 2 个
类型 : 便携电脑

-O 优化对程序的影响

m*m矩阵乘法的计算量为:
m m m + ( m 1 ) = m 2 ( 2 m 1 ) m*m*(m次乘法+(m-1)次加法)=m^2*(2m-1)次浮点运算
定义实际浮点性能和效率如下:
= 实际浮点性能 = \frac{程序计算量}{运行时间}
= 效率 = \frac{实际性能}{处理器峰值性能}

写一个c程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define m 1000

float a[m][m];
float b[m][m];
float c[m][m];
//不要使用大的局部变量(因为局部变量都分配在栈上),
//这样容易造成堆栈溢出,破坏系统的栈和堆结构,
//导致出现莫名其妙的错误。  所以要定义成全局变量 

int main() {
	struct timeval st, et;
	int i,j,k;
	srand((unsigned)time(NULL));//根据时间来产生随机数种子
	for(i=0; i<m; i++) {
		for(j=0; j<m; j++) {
			a[i][j] = (float)rand()/(RAND_MAX);//产生[0,1]随机数
			b[i][j] = (float)rand()/(RAND_MAX);
			c[i][j] = 0;//初始化c矩阵用来存结果
#if m <= 15   //打印一下呗 
			printf("%f ",a[i][j]);
			if(j==m-1) {
				printf("\n");
			}
#endif
		}
	}
//	int gettimeofday(struct timeval*tv, struct timezone *tz);
	gettimeofday(&st,NULL);
	for(i=0; i<m; i++) {
		for(j=0; j<m; j++) {
			for(k=0; k<m; k++) {
				c[i][j] = c[i][j] + a[i][k]*b[k][j] ;

			}

		}
	}
	gettimeofday(&et,NULL);
	printf("matrix multiply time: %0.6lf sec\n", et.tv_sec+et.tv_usec*1e-6-st.tv_sec-st.tv_usec*1e-6);//tv_sec表示秒, tv_usec表示微妙
	return 0;


}

我们用如下方式来计时:

#include <time.h>
struct timeval st, et;
gettimeofday(&st,NULL);

计时程序主体

gettimeofday(&et,NULL);
printf("matrix multiply time: %0.6lf sec\n", et.tv_sec+et.tv_usec*1e-6-st.tv_sec-st.tv_usec*1e-6);

在不同程度优化下,性能的比较:

优化选项 运行时间 性能(Mflops) 效率
-O0 16.831s 118.7689 0.0046
-O1 17.854 111.9637 0.0044
-O2 17.753 112.6007 0.0044
-O3 4.343s 460.2809 0.0180

根据结果我们发现在-O3之前,没有一个本质的变化,-O3后速度瞬间提升了4倍。另一观察到的是,运行的效率没有一个超过2%的,也就是说,计算机的性能没有发挥到极致,甚至只用了冰山一角,这也让我对并行有了更加急切的渴望,希望能发挥出那另外的98%的计算能力。
根据"天下没有免费的午餐"定理,这也是一个trade-off的过程。编译优化往往会给调试带来问题,并且因内存操作顺序的改变会造成许多数据不一致等问题。

gcc提供了为了满足用户不同程度的的优化需要,提供了近百种优化选项,用来对 {编译时间,目标文件长度,执行效率} 这个三维模型进行不同的取舍和平衡。优化的方法不一而足,总体上将有以下几类:1)精简操作指令;2)尽量满足cpu的流水操作;3)通过对程序行为地猜测,重新调整代码的执行顺序;4)充分使用寄存器;5)对简单的调用进行展开等等……

人为修改循环执行顺序对程序性能的影响

我们知道,改变i、j、k循环的先后顺序,不影响程序的结果,我们来看看改变ijk循环顺序后程序性能的改变。

依然采用原来的程序以及计时方式,只是调换乘法步骤ijk的执行顺序。

结果如下:

顺序 运行时间 性能(Mflops) 效率
ijk 9.673s 206.6577 0.0081
ikj 3.778s 529.1159 0.0207
jik 16.108s 124.0998 0.0048
jki 33.400s 59.8503 0.0023
kij 3.912s 510.9918 0.0200
kji 32.990s 60.5941 0.0024

出乎意料的是,以我们线性代数常性思维的方式所认同的i、j、k循环排序方式所用的时间是相当长的,而将j循环放到最后来执行能大大缩短时间。这也为我们在矩阵规模比较大时,程序的改进提供了方向。事实上,上面提到-O3优化已经考虑了ijk顺序的问题。

究其原因,是因为从一级缓存中提取中间结果的速度远大于低级的缓存,某些循环顺序巧妙利用了计算机的这种Cache结构。另外某些结果的计算依赖于前面计算的结果,在前面结果未出时,某些计算就开始排队等待……计算顺序的不合理安排,导致了计算时间的不必要增加。***并行计算***似乎就是基于这个理念进行程序性能的优化的。


心得与体会

1、不要使用大的局部变量(因为局部变量都分配在栈上),这样容易造成堆栈溢出,破坏系统的栈和堆结构,导致出现莫名其妙的错误。 所以大矩阵要尽量定义成全局变量 。
2、因只考虑矩阵乘法,我们计算时间不应该考虑矩阵生成和赋值的消耗。对于局部程序计时,可以使用gettimeofday方法,获取系统时间,前后相减。
3、因为每次随机数都是随机产生,即使是同一个可执行文件,同一次执行的时间也不尽相同,故而3.778s和3.912s甚至和4.343s没有本质上的区别。
4、从结果上,我们能体会到,我们平时做简单计算,其实并没有发挥计算机的全部计算能力,如何优化程序,让效率尽可能低提高,就显得尤为重要。

猜你喜欢

转载自blog.csdn.net/lusongno1/article/details/82824381