欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【MPI学习笔记】3:快速并行方阵和向量乘积+多机测试

程序员文章站 2022-07-12 21:33:56
...

简述

之前使用的是在一台机器上的,内存非常有限,而核心数也不是很多,为了减小机器承受的压力,每运行到某块*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进程下测试了三次,时间的单位是秒:
【MPI学习笔记】3:快速并行方阵和向量乘积+多机测试

【MPI学习笔记】3:快速并行方阵和向量乘积+多机测试

【MPI学习笔记】3:快速并行方阵和向量乘积+多机测试
我试过,如果直接把上一次(没有做速度上的优化时)的代码-O3编译,然后4主机48进程下跑(也就是说单纯依赖核心数的增加,而不对代码做速度上的优化)时,这个时间是380秒左右,相比之下,依赖重构在速度上优化了一半左右的时间。

当然,对速度的追求,造成了内存的浪费。如果真的有OPT算法,那么内存的浪费也完全没有了(可惜只能想想)。