一、一些概念
1、分布式内存系统:核与内存一一对应,内存只能被相连的核访问。
2、共享内存系统:核与内存是多对一的关系,多个核连在一个内存上,每个核可以访问内存的任意位置。
3、MPI标识符规范:都由MPI_开始,下划线后第一个字母大写,表示MPI定义的类型和函数名,MPI定义的宏和常量的所有字母都是大写的。
4、通信子:一组可以互相发送消息的进程集合。MPI_Init的一个目的是在用户启动程序时,定义由用户启动的所有进程所组成的通信子。这个通信子成为MPI_COMM_WORLD,即通信域。
5、SPMD程序(单程序多数据流程序):编写一个单个程序,让不同进程产生不同动作,简单的让进程按照他们的进程号来匹配程序分支。
6、MPI数据类型
7、消息匹配
进程接受消息的条件:recv_type=send_type,同时recv_buf_sz>=send_buf_sz
接受消息的顺序问题:当MPI_Recv函数中的source=MPI_ANY_SOURCE时它就可以按照进程完成的顺序来接受结果了
接受同进程不同标签消息的问题:当MPI_Recv函数中的tag=MPI_ANY_TAG时,可以解决这个问题。
注:只有接受者可以使用通配符参数,发送者必须指定一个进程号与一个非负整数标签
通信子参数没有通配符
8、集合通信:在MPI里涉及通信子中所有进程的通信函数
MPI_Recv和MPI_Send通常称为点对点通信
9、MPI中预定的规约操作符
10、集合通信与点对点通信
(1)在通信子中的所有进程都必须调用相同的集合通信函数。
(2)每个进程传递给MPI集合通信函数的参数必须是“相容的”。
(3)参数output_data_p只用在dest_process上,所有进程仍需要传递一个与output_data_p相对应的实际参数,及时它的值只是NULL。
(4)点对点通信函数是通过标签和通信子来匹配的,集合通信函数不使用标签,只通过通信子和调用的顺序来进行匹配。
11、广播:在一个集合通信中,如果属于一个进程的数据被发送到通信子中的所有进程,这样的集合通信就叫做广播。
12、数据分发——向量的划分
块划分:将连续的定量个数的向量分量所构成的块分配到每个进程中;
循环划分:用轮转的方式去分配向量分量。
块—循环划分:用一个循环来分发向量分量所构成的块,而不是分发单个向量分量,需要先决定块的大小。
13、派生数据类型:相当于C语言中的结构体。用于表示内存中数据项的任意集合。
表示形式:{(MPI_Datatype,int),(MPI_Datatype,int)(MPI_Datatype,int)},每一对数据项的第一个元素表名数据类型,第二个元素是该数据项相对于起始位置的偏移。
14、加速比:经常用来衡量串行运算和并行运算时间之间的关系,表示为串行时间与并行时间的比值,S(N,P)=T串行(n)/T并行(n,p)
线性加速比:S(N,P)=p,说明拥有comm_sz=p个进程数的并行程序能运行的比串行程序快p倍,这种加速比事实上很少出现。
15、效率:评价并行性能的重要指标之一,是每个进程的加速比,
E(N,P)=S(N,P)/p=T串行(n)/p*T并行(n,p)
16、对于上述两个指标,在p较小,n较大的情况下,有近似线性的效率,在p较大,n较小的情况下远远达不到线性效率
17、可扩展性:粗略的讲,如果问题的规模以一定的速率增大,但效率没有随着进程数的增加而降低,那么就可以认为程序是可扩展的
强可扩展性:程序可以在不增加问题规模的前提下维持恒定效率
弱可扩展性:当问题规模增加,通过增大进程数来维持程序效率
18、一个定理:设A是一个拥有n个键值的列表,作为奇偶交换排序算法的输入,那么经过n个阶段后,A能够排好序。
19、一个定理:如果由p个进程运行并行奇偶交换排序算法,则p个阶段后,输入列表排序完毕。
20、通信开销远比局部计算开销大。
21、MPI_PROC_NULL是由MPI库定义的一个常量。在点对点通信中,将它作为源进程或者目标进程的进程号,此时,调用通信函数后会直接返回,不会产生任何通信。
二、MPI编程代码
1、编译与执行
编译程序:
$ mpicc -g -Wall -o mpi_hello mpi_hello.c #命令行代码
//mpicc:编译器包装脚本;-o name:name表示创建的可执行文件的文件名。
启动程序:
$ mpiexec -n <number of processes> ./mpi_hello #命令行代码
//mpi为启动命令;-n为固定字符;<number of processes>:启动运行程序的进程个数,是一个整数;./name:name为要执行的程序名。
C语言头文件
#include<mpi.h>
//除其他必要头文件外必须包含此头文件,此头文件包括了MPI函数的原形、宏定义、类型定义等,还包括了编译MPI程序所需要的全部定义与声明
MPI初始化设置函数
int MPI_Init(int* argc_p,char*** argv_p);
//参数argc_p和argc_v是指向参数argc和argv的指针,当程序不使用这些参数时可直接写为NULL。
//该函数的返回值一般为一个整型int,我们一般忽略。
//所以一般最常见的用法为:
MPI_Init(NULL,NULL);
//除此之外,该函数一般表示MPI函数的开始,在此之前不应该有其他MPI函数
MPI使用完毕设置函数
int MPI_Finalize(void);
//该函数是为了告诉MPI系统MPI已经之心完毕,可以释放为MPI分配的资源了。
//在此函数之后不应该再出现MPI函数
MPI程序基本框架
...
#include<mpi.h>
...
int main(int argc,char* argv[]){
...
/*no MPI calls before this*/
MPI_Init(&argc,&argv);
...
MPI_Finalize();
/*no MPI calls after this*/
...
return 0;
}
//我们并不一定要向MPI_Init传递参数,也不一定要在main函数中调用MPI_Init和MPI_Finalize
获取当前通信域的进程数
int MPI_Comm_size(MPI_Comm comm /*in*/,int* comm_sz_p /*out*/);
//comm是一个通信子,类型为MPI为通信子定义的特殊类型
//在comm_sz_p中返回通信子的进程数
获取正在调用进程在通信子中的进程号
int MPI_Comm_rank(MPI_Comm comm /*in*/,int* my_rank_p /*out*/);
//comm是一个通信子,类型为MPI为通信子定义的特殊类型
//在my_rank_p中返回正在调用进程在通信子中的进程号
//在MPI_COMM_WORLD中经常用参数comm_sz表示进程的数量,用参数my_rank来表示进程号
进程执行发送的函数
int MPI_Send(void* msg_buf_p /*in*/,int msg_size /*in*/,MPI_Datatype msg_type /*in*/,
int dest /*in*/,int tag /*in*/,MPI_Comm communicator /*in*/);
//前三个参数定义了消息的内容,后三个参数定义了消息的目的地
//第一个参数是一个指向包含消息内容的内存块的指针
//第二个和第三个参数指定了要发送的数据量,分别表示消息字符串字符数量加上'\0'(字符结束符,即+1),发送的数据类型详细数据类型参照MPI数据类型表
//第四个参数指定了要接受消息的进程号
//第五个参数为一个非负int型变量,用于区分看上去完全一样的消息
//第六个参数是一个通信子,所有涉及通信的MPI都有一个通信子参数,用来指定通信范围,防止一个通信子中的进程发送的消息被另一个通信子中的进程接收
进程执行接受的函数
int MPI_Recv(void* msg_buf_p /*out*/,int buf_size /*in*/,MPI_Datatype buf_type /*in*/,
int source /*in*/,int tag /*in*/,MPI_Comm communicator /*in*/,
MPI_Status* status_p /*out*/);
//msg_buf_p:指向内存块
//buf_size:指定内存块中要存储对象的数量
//buf_type:说明了对象的类型
//source:指定了接受的消息应该从哪个进程发送过来
//tag:与发消息的tag匹配
//communicator:必须与发送消息的进程的通信子相匹配
//status_p:一般不使用这个参数,一般赋予MPI常量MPI_STATUS_IGNORE就可以
找回接收到的数据量
int MPI_Get_Count(MPI_Status* status_p /*in*/,MPI_Datatype type /*in*/,
int* count_p /*out*/)
//会返回count_p参数接收到的元素数量
规约函数
int MPI_Reduce(void* input_data_p /*in*/,void* output_data_p /*out*/,int count /*in*/,
MPI_Datatype datatype /*in*/,MPI_Op operator /*in*/,
int dest_process /*in*/,MPI_Comm comm /*in*/);
//函数关键在于第五个参数,详细情况见表MPI中预定的规约操作符
//dest_process指定了接受存储结果的进程编号
让通信子中的所有进程都存储结果
int MPI_Allreduce(void* input_data_p /*in*/,void* output_data_p /*out*/,int count /*in*/,
MPI_Datatype datatype /*in*/,MPI_Op operator /*in*/,
MPI_comm comm /*in*/);
广播函数
int MPI_Bcast(void* data_p /*in/out*/,int count /*in*/,MPI_Datatype datatype /*in*/,
int source_proc /*in*/,MPI_Comm comm /*in*/);
//source_proc是发送内容的进程编号
//data_p是要发送的内容,该内容会发送给comm中的所有进程
散射函数:某个进程读入全部数据,但只将部分数据发送给需要分量的其他进程
int MPI_Scatter(void* send_buf_p /*in*/,int send_count /*in*/,
MPI_Datatype send_type /*in*/,void* recv_buf_p /*out*/,
int recv_count /*in*/,MPI_Datatype recv_type /*in*/,
int src_proc /*in*/,MPI_Comm comm /*in*/);
//如果通信子comm包含comm_sz个进程,那么该函数会将send_buf_p所引用的数据分成comm_sz份,并将第i份分给第i-1号进程。
//send_count:发送到每个进程的数据量
//recv_count=n/comm_sz,通常指每个进程结束的数据量,大部分情况下与send_count的值相同
聚集函数:将其他进程的结果收集到某个进程上
int MPI_Gather(void* send_buf_p /*in*/,int send_count /*in*/,
MPI_Datatype send_type /*in*/,void* recv_buf_p /*out*/,
int recv_count /*in*/,MPI_Datatype recv_type /*in*/,
int dest_proc /*in*/,MPI_Comm comm /*in*/);
//recv_count:每个进程接收到的数据量
全局聚集函数:将每个进程的send_buf_p内容串联起来,存储到每个进程的recv_buf_p参数中
int MPI_Allgather(void* send_buf_p /*in*/,int send_count /*in*/,
MPI_Datatype send_type /*in*/,void* recv_buf_p /*out*/,
int recv_count /*in*/,MPI_Datatype recv_type /*in*/,
MPI_Comm comm /*in*/);
//与MPI_Gather相比少了一个指定聚集目的地址的dest_proc参数
创建由不同基本数据类型的元素所组成的派生数据类型
int MPI_Type_create_struct(int count /*in*/,int array_of_blocklengths[] /*in*/,
MPI_Aint array_of_displacements[] /*in*/,
MPI_Datatype array_of_types[] /*in*/,MPI_Datatype* new_type_p /*out*/);
//count指的是数据类型中元素的个数,每个数组参数都有count个元素
//array_of_blocklengths[]:允许单独的数据项,可能是一个数组或子数组。
//array_of_displacements[]:指定了距离消息起始位置的偏移量,单位为字节。
//array_of_types[]:存储的是元素的MPI数据类型
//new_type_p:是我们定义的新的数据类型的名称
寻找内存单元地址的函数
int MPI_Get_address(void* location_p /*in*/,MPI_Aint* address_p /*out*/);
//返回location_p所指向的内存单元的地址
//MPI_Aint是整数型,它的长度足以表示系统地址,也是返回值
指定新数据类型的函数
int MPI_Type_commit(MPI_Datatype* new_mpi_p /*in/out*/);
//在使用MPI_Type_create_struct函数的第五个参数之前我们要先调用此函数去指定它
释放新数据类型额外的存储空间
int MPI_Type_free(MPI_Datatype* old_mpi_t_p /*in/out*/);
计时函数:返回从过去某一时刻开始所经过的秒数
double MPI_Wtime(void);
确保进程同时返回函数:确保同一个通信子中的所有进程都完成调用该函数之前,没有进程能够提前返回
int MPI_Barrier(MPI_Comm comm /*in*/);
同步发送函数:可以用这个函数来代替MPI_Send来检查程序是否安全
int MPI_Ssend(void* msg_buf_p /*in*/,int msg_size /*in*/,MPI_Datatype msg_type /*in*/,
int dest /*in*/,int tag /*in*/,MPI_Comm communicator /*in*/);
//前三个参数定义了消息的内容,后三个参数定义了消息的目的地
//第一个参数是一个指向包含消息内容的内存块的指针
//第二个和第三个参数指定了要发送的数据量,分别表示消息字符串字符数量加上'\0'(字符结束符,即+1),发送的数据类型详细数据类型参照MPI数据类型表
//第四个参数指定了要接受消息的进程号
//第五个参数为一个非负int型变量,用于区分看上去完全一样的消息
//第六个参数是一个通信子,所有涉及通信的MPI都有一个通信子参数,用来指定通信范围,防止一个通信子中的进程发送的消息被另一个通信子中的进程接收
自己调度通信的函数
int MPI_Sendrecv(void* send_buf_p /*in*/,int send_buf_size /*in*/,
MPI_Datatype send_buf_type /*in*/,int dest /*in*/,
int send_tag /*in*/,void* recv_buf_p /*out*/,
int recv_buf_size /*in*/,MPI_Datatype recv_buf_type /*in*/,
int source /*in*/,int recv_tag /*in*/,MPI_Comm communicator /*in*/,
MPI_Status* status_p /*in*/);
//调用一次这个函数它会分别执行一次阻塞式消息发送和一次消息接收
//dest和source参数可以相同也可以不同
int MPI_Sendrecv_replace(void* buf_p /*in/out*/,int buf_size /*in*/,
MPI_Datatype buf_type /*in*/,int dest /*in*/,
int send_tag /*in*/,int source /*in*/,
int recv_tag /*in*/,MPI_comm communicator /*in*/,
MPI_Status* status_p /*in*/);
//如果发送和接收使用的是同一个缓冲区,可以使用该函数,作用和上述函数相同