【MPI学习笔记】3:快速并行方阵和向量乘积+多机测试
简述
之前使用的是在一台机器上的,内存非常有限,而核心数也不是很多,为了减小机器承受的压力,每运行到某块*alloc出的内存必定不被使用时,就立即free掉,而在多机上,这样的压力分散到了多台机器上。按照这次作业的要求,需要让计算速度尽可能快,这样就应当能不free的尽量不free,能不同步的不做同步,从而快速得到结果,再去free申请来的内存。
程序中还有一些细节,如当j=0时,操作row_now[j]必定会比操作row_now[0]慢,因为对j的取值解析也需要一次访问内存,为了至高的速度考虑,程序中应尽量避免这样的浪费。
上次课时,何老师指出,不必要的calloc也是一种浪费,因为对内存的清空,即初始化全0,也是要耗费时间的。在个问题中这样做是冗余的,因此只需要在j=0时对其做赋值,而其它时候做+=操作。
另外,如果在for循环中只对游标等于某个数值时做if判断,是非常不值得的,因为if的条件判断也是要耗费时间的,所以这部分被我重构了。
代码
注释中,用old:标注的是之前的做法和考虑,new:标注是新的做法和考虑。
/*
15121856 刘知昊
第2次作业(方阵x向量按行分配,计时)
*/
#include<stdio.h>
#include<mpi.h>
#include<stdlib.h>
#include<time.h>
//方阵的维度
#define N 960000
time_t start,end;//开始和结束时间
int main()
{
int *vec=NULL;//列向量
//double *mat=NULL;//自己进程的那部分矩阵
int my_rank;//自己进程的进程号
int comm_sz;//总的进程数目
int my_row;//本进程处理的行数
int i,j;//通用游标
double *result=NULL;//用来存本进程计算出的结果向量
double *all_rst=NULL;//只给0号进程存总的结果
double *row_now=NULL;//每个进程每次仅申请的一行
/**********************/
MPI_Init(NULL,NULL);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);
//计时开始
if(my_rank==0)
{
start=time(NULL);
}
//本进程处理的行数就是总阶数/进程数
my_row=N/comm_sz;
//为每个进程都申请空间
vec=malloc(N*sizeof(int));
//mat=malloc(N*my_row*sizeof(double));//这是个进程每次申请完
row_now=malloc(N*sizeof(double));//这是每个进程每次仅申请一行
result=malloc(my_row*sizeof(double));//old:为了初始值置0而使用calloc
//new:为了效率而用malloc,只要在i=0时做赋值
//向量的值的设定,使用memset会报错
for(j=0;j<N;j++)
vec[j]=1;
//本行的值的设定
//在实际使用时,这里是读入本进程应处理的下一行
for(j=0;j<N;j++)
row_now[j]=j;
//复杂度O(N*my_row)=O(N*N/comm_sz)
for(i=0;i<my_row;i++)
{
/*
//本行的值的设定
//在实际使用时,这里是读入本进程应处理的下一行
for(j=0;j<N;j++)
row_now[j]=j;
*/
//j=0时做赋值,后面就都加上来,这样就不需malloc
result[i]=row_now[0]*vec[0];
for(j=1;j<N;j++)
{
/*old:
//计算并加入到本进程结果向量的对应位置上
if(j==0)
result[i]=row_now[0]*vec[0];//用0比用j少一次向内存寻址!
*/
//上面这种方式的缺陷在于,每次都要多执行一下if判断
//即使是执行简单的if判断,在数量级很大时也是很耗时间的
//所以拿到函数体外
//计算并加入到本进程结果向量的对应位置上
result[i]+=row_now[j]*vec[j];
}
//下次循环直接覆盖上次使用的值
//因此不需free和重新申请row_now
}
//old:为了机器考虑,确定不再使用的空间立即free掉
//free(row_now);
//new:为了效率考虑,这里先不free
//while(0){}//测试用
//old:此处的进程同步是必要之举
//MPI_Barrier(MPI_COMM_WORLD);
//new:并不必要,Gather本身会去管理进程传来信息的位置
//聚集给0号进程
if(my_rank==0)
{
//先开辟存储空间
all_rst=malloc(N*sizeof(double));
//聚集
MPI_Gather
(
result, /*发送内容的地址*/
my_row, /*发送的长度*/
MPI_DOUBLE, /*发送的数据类型*/
all_rst, /*接收内容的地址*/
my_row, /*接收的长度*/
MPI_DOUBLE, /*接收的数据类型*/
0, /*接收至哪个进程*/
MPI_COMM_WORLD /*通信域*/
);
}
else
{
//聚集
MPI_Gather
(
result, /*发送内容的地址*/
my_row, /*发送的长度*/
MPI_DOUBLE, /*发送的数据类型*/
all_rst, /*接收内容的地址*/
my_row, /*接收的长度*/
MPI_DOUBLE, /*接收的数据类型*/
0, /*接收至哪个进程*/
MPI_COMM_WORLD /*通信域*/
);
}
//进程同步,让所有进程的资源都准备好
//MPI_Barrier(MPI_COMM_WORLD);
//new:不必要
//old:为了机器考虑,确定不再使用的空间立即free掉
//free(vec);
//free(result);
//free(mat);
//new:为了效率考虑,这里先不free
//0号进程负责输出
if(my_rank==0)
{
//计时结束
end =time(NULL);
//计算时间
printf("time=%f\n",difftime(end,start));
printf("The Result is:\n");
//改变跨度可以采样获取结果,快速结束I/O
for(i=0;i<N;i+=N/10)
printf("%f\n",all_rst[i]);
}
MPI_Finalize();
/**********************/
//最终,free应无遗漏
free(all_rst);
//new:
free(row_now);
free(vec);
free(result);
return 0;
}
测试
因为有其他同学也在用,他们在做什么事会直接影响我测试出的时间,在4主机48进程下测试了三次,时间的单位是秒:
我试过,如果直接把上一次(没有做速度上的优化时)的代码-O3编译,然后4主机48进程下跑(也就是说单纯依赖核心数的增加,而不对代码做速度上的优化)时,这个时间是380秒左右,相比之下,依赖重构在速度上优化了一半左右的时间。
当然,对速度的追求,造成了内存的浪费。如果真的有OPT算法,那么内存的浪费也完全没有了(可惜只能想想)。
上一篇: (二)MS-MPI多机通信
下一篇: MPI 3多机安装