消息传递接口(Message Passing Interface,MPI):定义了一个可以被C/C++/Fortran程序调用的函数库,来实现进程之间的通信。即MPI不是一种编程语言,而是一个编程接口标准。
OpenMP/Pthreads与MPI区别:前者是共享式内存(单节点)并行,后者是分布式内存(多节点)并行。
编译与执行方法:MPICH是MPI的一个重要实现,linux下安装命令为sudo apt-get install -y mpich,编译命令为mpicc -o xxx xxx.c,执行命令为mpiexec -n 进程数量 ./xxx。
一、基本函数//初始化函数:系统进行必要的初始化设置,如为消息缓冲区分配空间,为进程指定进程号等 //参数argc_p和argv_p分别指向main函数参数argc、argv,若不需要则设为NULL int MPI_Init(int* argc_p, char*** argv_p); //终止函数:告知系统MPI使用完毕,相关资源可以释放(但调用后进程仍然存在) int MPI_Finalize(); //通信子:一组可以互相发送消息的进程集合,类型为MPI_Comm; //MPI_COMM_WORLD:由用户启动的所有进程所组成的通信子 int comm_sz, my_rank; //通信子大小,进程号 int MPI_Comm_size(MPI_Comm comm, int* comm_sz_p); //获取通信子大小 int MPI_Comm_rank(MPI_Comm_comm, int* my_rank_p); //获取当前进程号点到点通信
//发送函数
int MPI_Send(
void* msg_buf_p,
int msg_size,
MPI_Datatype msg_type,
int dest,
int tag,
MPI_Comm comm
);
//接收函数
int MPI_Recv(
void* msg_buf_p,
int msg_size,
MPI_Datatype msg_type,
int source,
int tag,
MPI_Comm comm,
MPI_Status* status_p
);
//status_p参数使用说明
MPI_Status status; //定义status
...... //将&status作为参数传递给MPI_Recv函数
status.SOURCE //获取发送者进程号
status.TAG //获取消息标签
int count; //消息数据量
MPI_Get_count(&status, msg_type, &count) //获取数据量
//int MPI_Get_count(MPI_Status* status_p, MPI_Datatype type, int* count_p);
发送与接收函数的额外说明:
- 消息匹配:数据类型、标签值、通信子要相同,进程号要对应,recv_msg_size不小于send_msg_size。
- 通配符:对于接收函数,可以将第四个参数写为常量MPI_ANY_SOURCE来接收任意进程发来的消息,可以将第五个参数写为MPI_ANY_TAG来接收任意标签的消息。
- status_p参数:如果不使用,将其设为MPI_STATUS_IGNORE。什么时候使用呢?在接收者不知道发送者的进程号、发送消息的标签、或者消息的数据量时进行使用。
- 阻塞与非阻塞通信:留坑
//示例:test.c #include#include #include const int MAX_LEN = 100; int main(int argc, char* argv[]) { char send_buf[MAX_LEN], recv_buf[MAX_LEN]; int comm_sz, my_rank; MPI_Init(NULL, NULL); MPI_Comm_size(MPI_COMM_WORLD, &comm_sz); MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); if (my_rank != 0) { sprintf(send_buf, "Greetings from process %d of %d!n", my_rank, comm_sz); MPI_Send(send_buf, strlen(send_buf), MPI_CHAR, 0, 0, MPI_COMM_WORLD); } else { printf("This is process %d of %d!n", my_rank, comm_sz); for (int i = 1; i < comm_sz; i++) { MPI_Recv(recv_buf, MAX_LEN, MPI_CHAR, MPI_ANY_SOURCE, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); printf("%sn", recv_buf); } } return 0; } //编译:mpicc -o test test.c //执行:mpiexec -n 4 ./test //输出(也可能是其他顺序,如132等) This is process 0 of 4! Hello World from process 1 of 4! Hello World from process 2 of 4! Hello World from process 3 of 4!
关于I/O:
- 输入:大多数MPI实现只允许0号进程接收输入,若有需要,0号进程再将消息发送给其他进程。
- 输出:由于大多数MPI实现不提供对I/O设备的调度,多个进程同时写标准输出stdout时,会抢占竞争资源,输出顺序无法确定。若不希望出现乱序输出,可以让0号进程外的其他进程向0号进程发送它的输出,然后0号进程按照进程号的顺序打印输出。
点对点通信:发送和接收是一个进程到一个进程。集合通信:涉及通信子中所有进程的通信函数。
归约函数//函数对输入数据进行操作(operator),并将输出数据存储到目的进程(dest_process)
int MPI_Reduce(
void* input_data_p,
void* output_data_p, //只用在dest_process上,其他进程传递NULL
int count,
MPI_Datatype datatype,
MPI_Op operator, //MPI_Op也是个预定义类型,包含多个归约操作符
int dest_process,
MPI_Comm comm
);
//MPI_Reduce中只有目的进程得到结果,MPI_AllReduce使每个进程都得到结果
int MPI_AllReduce(
void* input_data_p,
void* output_data_p,
int count,
MPI_Datatype datatype,
MPI_Op operator,
MPI_Comm comm
);
广播函数
//一个进程中的数据被发送到通信子中的其他所有进程
int MPI_Bcast(
void* data_p,
int count,
MPI_Datatype datatype,
int source,
MPI_Comm comm
);
散射函数
//散射函数:将send_buf_p所指内容分成comm_sz份,第一份给0号进程,第二份给1号进程...
int MPI_Scatter(
void* send_buf_p,
int send_count, //为local_n=n/comm_sz
MPI_Datatype send_type,
void* recv_buf_p,
int recv_count, //为local_n=n/comm_sz
MPI_Datatype recv_type,
int source,
MPI_Comm comm
);
聚集函数
//聚集函数:与散射函数相对应,各个进程将自己的数据聚集到目的进程
int MPI_Gather(
void* send_buf_p,
int send_count,
MPI_Datatype send_type,
void* recv_buf_p,
int recv_count,
MPI_Datatype recv_type,
int dest,
MPI_Comm comm
);
//全局聚集函数:聚集结果所有进程均得到(参数列表不变)
int MPI_Allgather(...);
三、派生数据类型
在分布式内存系统中,通信的开销远远大于计算,并且使用多条消息发送一定数量的数据,明显比一条消息发送等量数据耗时。因此,如果可以减少消息的数量,就能够提高程序的性能。
派生数据类型:通过存储数据项的类型以及它们在内存中的相对位置(以字节为单位),来表示内存中数据项的任意集合(为什么不用结构体呢?因为MPI_Datatype没有定义结构体,而通信函数参数类型确是MPI_Datatype,所以需要自己进行创建封装)。
int MPI_Type_create_struct(
int count, //元素个数,后面的数组参数都有count个元素
int array_of_blocklengths[], //表示count个元素中每个元素长度,如第一个元素为长度为m的数组,则array_of_blocklengths[0]=m
MPI_Aint array_of_displayments[], //MPI_Aint:整数型,可以表示系统地址,数组存储各元素距离消息起始位置的偏移量(以字节为单位)
MPI_Datatype array_of_types[], //表示各个元素的数据类型
MPI_Datatype* new_type_p //输出
);
//需要说明的是:
//1.使用input_mpi_t之前,必须先用一个函数调用指定它:
int MPI_Type_commit(MPI_Datatype* new_mpi_t_p);
//2.若input_mpi_t不再使用,可以释放内存空间:
int MPI_Type_free(MPI_Datatype* old_mpi_t_p);
四、时间性能
MPI_Wtime函数返回墙上时钟时间(经历的全部时间,包括空闲等待时间),C/C++的clock函数返回的是CPU时间(用户代码、库函数和系统调用函数消耗的时间),不包括空闲时间。
//MPI程序计时方法 //函数原型:double MPI_Wtime(void); double start, finish; start = MPI_Wtime(); ... finish = MPI_Wtime();
这样做,每个进程都会给出一个时间,而我们需要的是一个总的单独时间。理想情况是,所有进程同时开始运行,并行时间取决于“最慢”进程。
double local_start, local_finish, local_elapsed, elapsed; MPI_Barrier(comm); //同步函数:直到所有进程都调用了该函数后才继续执行 local_start = MPI_Wtime(); ... local_finish = MPI_Wtime(); local_elapsed = local_finish - local_start; MPI_Reduce(&local_elapsed, &elapsed, 1, MPI_DOUBLE, MPI_MAX, 0, comm); if(my_rank == 0) printf...



