前言:
MPI(Message Passing Interface)是一个跨语言的通讯协议,用于编写并行程序。与OpenMP并行程序不同,MPI是一种基于消息传递的并行编程技术。消息传递接口是一种编程接口标准,而不是一种具体的编程语言。 简而言之,MPI标准定义了一组具有可移植性的编程接口。不同的厂商和组织遵循着这个标准推出各自的实现,而不同的实现也会有其不同的特点。 由于MPI提供了统一的编程接口,程序员只需要设计好并行算法,使用相应的MPI库就可以实现基于消息传递的并行计算。MPI支持多种操作系统,包括大多数的类UNIX和Windows系统。
1.第一个MPI程序
这里以C语言为例开始你的第一个MPI程序!
首先,我们应该先包含进一个头文件<mpi.h>
,我们使用的函数都在其中。另外,在这之后,MPI程序和普通的C程序的区别在于有一个开始的函数和结束的函数来标识MPI部分,再在这个部分进行你想要进行的操作,现在就来尝试一下!
需要特别说明的是,本章节及之后的所有章节的例程都是假设进程数为4的前提下进行的。
虽然目前来说输出和普通程序没什么区别,但你已经走出了第一步!
函数说明:
int MPI_Init(int *argc, char **argv)
通过MPI_Init函数进入MPI环境并完成所有的初始化工作,标志并行代码的开始。
int MPI_Finalize(void)
通过MPI_Finalize函数从MPI环境中退出,标志并行代码的结束,如果不是MPI程序最后一条可执行语句,则运行结果不可知。
#include <mpi.h>
#include <stdio.h>
int main(int argc, char **argv)
{
MPI_Init(&argc, &argv);
printf("Hello World!\n");
MPI_Finalize();
return 0;
}
2.
获取进程数量
在MPI编程中,我们常常需要获取指定通信域的进程个数,以确定程序的规模。
一组可以相互发送消息的进程集合叫做通信子,通常由MPI_Init()
在用户启动程序时,定义由用户启动的所有进程所组成的通信子,缺省值为 MPI_COMM_WORLD 。这个参数是MPI通信操作函数中必不可少的参数,用于限定参加通信的进程的范围。
函数说明:
int MPI_Comm_size(MPI_Comm comm, int *rank)
获取指定通信域的进程个数。
其中,第一个参数是通信子,第二个参数返回进程的个数。
实验说明:
使用函数MPI_Comm_size获取通信域中的进程个数并打印出来。
#include <stdio.h>
#include <mpi.h>
int main(int argc, char **argv)
{
int numprocs;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
printf("Hello World! The number of processes is %d\n",numprocs);
MPI_Finalize();
return 0;
}
3.
获取进程id
同样,我们也常常需要输出当前进程的id,以此来判断具体哪个进程完成了对应的任务。
本章节同时也是对上一章节的强化复习。
函数说明:
int MPI_Comm_rank(MPI_Comm comm, int *rank)
获得当前进程在指定通信域中的编号,将自身与其他程序区分。
其中,第一个参数是通信子,第二个参数返回进程的编号。
实验说明:
在每个进程中,使用函数MPI_Comm_rank来获取当前进程的id并打印出来。
输出结果:
0
1
3
2
由于并行程序执行顺序的不确定性,你的结果的顺序可能和这个结果不一致。
#include <stdio.h>
#include <mpi.h>
int main(int argc, char **argv)
{
int myid, numprocs;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
printf("Hello World!I'm rank %d of %d\n", myid, numprocs);
MPI_Finalize();
return 0;
}
4.
获取处理器名
有时候在实际处理中我们可能需要将进程迁移至不同的处理器,而MPI提供了获取处理器名的函数以简单地允许这种行为。
注意在MPI中不需要定义这种迁移。
函数说明:
int MPI_Get_processor_name ( char *name, int *resultlen)
char *name : 实际节点的唯一说明字;
int *resultlen:在name中返回结果的长度;
返回调用时调用所在的处理器名。
实验说明:
在每个进程中,使用函数MPI_Get_processor_name来获取当前进程的处理器名并打印出来。
输出结果:
Hello, world. I am PROCESS_NAME.
#include <stdio.h>
#include <mpi.h>
int main(int argc, char **argv)
{
int len;
char name[MPI_MAX_PROCESSOR_NAME];
MPI_Init(&argc, &argv);
MPI_Get_processor_name (name, &len);
printf("Hello, world. I am %s.\n", name);
MPI_Finalize();
return 0;
}
5.
运行时间
在实际编程中,计时是一个很实用的功能。
在MPI编程我们可以使用MPI_Wtime函数在并行代码中计算运行时间,用MPI_Wtick来查看精度。
函数说明:
double MPI_Wtime(void)
double MPI_Wtick(void)
MPI_WTIME返回一个用浮点数表示的秒数, 它表示从过去某一时刻到调用时刻所经历的时间。
MPI_WTICK返回MPI_WTIME的精度,单位是秒,可以认为是一个时钟滴答所占用的时间。
实验说明:
使用函数MPI_Wtime计算并行代码的运行时间,并且在两次计算时间的函数之间用函数MPI_WTICK打印出精度。
输出结果:
输出结果格式应如下:
The precision is: 1e-06
Hello World!I'm rank ... of ..., running ... seconds.
#include<stdio.h>
#include<mpi.h>
int main(int argc, char **argv)
{
int myid, numprocs;
double start, finish;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
//your code here
start = MPI_Wtime();
printf("The precision is: %f\n", MPI_Wtick());
finish = MPI_Wtime();
//your code here
printf("Hello World!I'm rank %d of %d, running %f seconds.\n", myid, numprocs, finish-start);
MPI_Finalize();
return 0;
}
6.
同步
在实际工作中,我们常常会因为许多原因需要进行同步操作。
例如,希望保证所有进程中并行代码在某个地方同时开始运行,或者在某个函数调用结束之前不能返回。
这时候我们就需要使用到MPI_Barrier函数。
函数说明:
int MPI_Barrier(MPI_Comm comm)
MPI_Comm comm : 通信子;
阻止调用直到communicator中所有进程已经完成调用,就是说,任意一次进程的调用只能在所有communicator中的成员已经开始调用之后进行。
实验说明:
在计算运行时间的信息之前调用MPI_Barrier函数完成同步。
输出结果:
输出结果格式应如下:
The precision is: 1e-06
Hello World!I'm rank ... of ..., running ... seconds.
在此示例程序中,可能是否调用函数不影响最终输出,但这并不意味着效果相同。
#include<stdio.h>
#include<mpi.h>
int main(int argc, char **argv)
{
int myid, numprocs;
double start, finish;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
//your code here
MPI_Barrier(MPI_COMM_WORLD);
//end of your code
start = MPI_Wtime();
printf("The precision is: %f\n", MPI_Wtick());
finish = MPI_Wtime();
printf("Hello World!I'm rank %d of %d, running %f seconds.\n", myid, numprocs, finish-start);
MPI_Finalize();
return 0;
}
7.
消息传递
在并行编程中,消息传递占了很大的比重。良好的消息传递是正常完成进程/节点之间操作的基本条件。在这里先介绍的最基本发送/接收函数。
最基本的发送/接收函数都是以缓冲区作为端点,通过参数配置来完成指定操作。
函数说明:
int MPI_Send(void* msg_buf_p, int msg_size, MPI_Datatype msg_type, int dest, int tag, MPI_Comm communicator)
发送缓冲区中的信息到目标进程。
其中,
void* msg_buf_p : 发送缓冲区的起始地址;
int buf_size : 缓冲区大小;
MPI_Datatype msg_type : 发送信息的数据类型;
int dest :目标进程的id值;
int tag : 消息标签;
MPI_Comm communicator : 通信子;
int MPI_Recv(void* msg_buf_p, int buf_size, MPI_Datatype msg_type, int source, int tag, MPI_Comm communicator, MPI_Status *status_p)
发送缓冲区中的信息到目标进程。
其中,
void* msg_buf_p : 缓冲区的起始地址;
int buf_size : 缓冲区大小;
MPI_Datatype msg_type : 发送信息的数据类型;
int dest :目标进程的id值;
int tag : 消息标签;
MPI_Comm communicator : 通信子;
MPI_Status *status_p : status_p对象,包含实际接收到的消息的有关信息
实验说明:
在这里我们把id为0的进程当作根进程,然后在除此之外的进程中使用函数MPI_Send发送一句"hello world!"到根进程中,然后在根进程中把这些信息打印出来。
输出结果:
一系列的"hello world!"。
#include <stdio.h>
#include <mpi.h>
int main(int argc, char **argv)
{
int myid, numprocs, source;
MPI_Status status;
char message[100];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
if(myid != 0) {
strcpy(message, "hello world!");
//your code here
MPI_Send(message, strlen(message)+1, MPI_CHAR, 0, 99, MPI_COMM_WORLD);
//end of your code
}
else { //myid == 0
for(source=1; source<numprocs; source++) {
//your code here
MPI_Recv(message, 100, MPI_CHAR, source, 99, MPI_COMM_WORLD, &status);
//end of your code
printf("%s\n", message);
}
}
MPI_Finalize();
return 0;
}
8.
地址偏移量
在通信操作中,我们常常需要对地址进行传递或操作,例如传送/接收缓冲区。
而一个位置在内存中的地址可以通过MPI_ADDRESS函数获得。
函数说明:
int MPI_Address(void* location, MPI_Aint *address)
void* location : 调用者的内存位置;
MPI_Aint *address:位置的对应地址;
实验说明:
给出三个临时变量a, b, c, 分别求出a与b、a与c之间的地址偏移量。
输出结果:
由于这里采用的变量类型为int,所以如果变量地址是连续的话应该是:
The distance between a and b is 4
The distance between a and c is 8
#include<stdio.h>
#include<mpi.h>
int main(int argc, char **argv)
{
int myid, numprocs;
MPI_Aint address1, address2, address3;
int a, b, c, dist1, dist2;
a = 1;
b = 2;
c = 3;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
// your code here
MPI_Address(&a, &address1);
MPI_Address(&b, &address2);
MPI_Address(&c, &address3);
// end of your code
dist1 = address2 - address1 ;
dist2 = address3 - address1 ;
if(myid == 0) {
printf("The distance between a and b is %d\n", dist1);
printf("The distance between a and c is %d\n", dist2);
}
MPI_Finalize();
return 0;
}
9.
数据的打包(pack)
有时候我们希望将不连续的数据或是不相同的数据类型的数据一起发送到其他进程,而不是效率很低地逐个发送。
一个解决这个问题的方案是将数据封装成包,再将数据包放到一个连续的缓冲区,发送到接收缓冲区后再提取出来尽心解包。
值得注意的是,打包/解包函数有时候还会用来代替系统缓存策略。此外,对于在MPI顶层进一步开发附加的通信库会起到辅助的作用。
函数说明:
int MPI_Pack(void* inbuf, int incount, MPI_datatype datatype, void *outbuf, int outcount, int *position, MPI_Comm comm)
void* inbuf : 输入缓冲区地址;
int incount :输入数据项数目;
MPI_datatype datatype :数据项的类型;
void *outbuf :输出缓冲区地址;
int outcount :输出缓冲区大小;
int *position :缓冲区当前位置;
MPI_Comm comm :通信子;
实验说明:
在源进程中打包发送一个数据包到进程1,进程1解包并打印出数据。
输出结果:
The number is 1 and 2
```
#include <stdio.h>
#include <mpi.h>
int main(int argc, char **argv)
{
int myid, numprocs, source;
MPI_Status status;
int i, j, position;
int k[2];
int buf[1000];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
i = 1;
j = 2;
if(myid == 0) {
position = 0 ;
// your code here
MPI_Pack(&i, 1, MPI_INT, buf, 1000, &position, MPI_COMM_WORLD);
MPI_Pack(&j, 1, MPI_INT, buf, 1000, &position, MPI_COMM_WORLD);
// end of your code
MPI_Send(buf, position, MPI_PACKED, 1, 99, MPI_COMM_WORLD);
}
else if (myid == 1){
MPI_Recv(k, 2, MPI_INT, 0, 99, MPI_COMM_WORLD, &status);
position = 0 ;
MPI_Unpack(k, 2, &position, &i, 1, MPI_INT, MPI_COMM_WORLD);
MPI_Unpack(k, 2, &position, &j, 1, MPI_INT, MPI_COMM_WORLD);
printf("The number is %d and %d", i, j);
}
MPI_Finalize();
return 0;
}
10.
数据的解包(unpack)
解包是对应于打包的MPI操作。
需要特别注意的是:MPI_RECV和MPI_UNPACK的区别: 在MPI_RECV中, count参数指明的是可以接收的最大项数. 实际接收的项数是由接收的消息的长度来决定的. 在MPI_UNPACK中, count参数指明实际打包的项数; 相应消息的"size"是position的增加值. 这种改动的原因是"输入消息的大小" 直到用户决定如何解包之前是不能预先确定的;从解包的项数来确定"消息大小"也是很困难的。
函数说明:
int MPI_Unpack(void* inbuf, int insize, int *position, void *outbuf, int outcount, MPI_Datatype datatype, MPI_Comm comm)
void* inbuf : 输入缓冲区地址;
int insize :输入数据项数目;
MPI_datatype datatype :数据项的类型;
void *outbuf :输出缓冲区地址;
int outcount :输出缓冲区大小;
int *position :缓冲区当前位置;
MPI_Comm comm :通信子;
实验说明:
在源进程中打包发送一个数据包到编号为1的进程,编号为1的进程解包并打印出数据。
输出结果:
The number is 1 and 2
```
#include <stdio.h>
#include <mpi.h>
int main(int argc, char **argv)
{
int myid, numprocs, source;
MPI_Status status;
int i, j, position;
int k[2];
int buf[1000];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
i = 1;
j = 2;
if(myid == 0) {
position = 0 ;
MPI_Pack(&i, 1, MPI_INT, buf, 1000, &position, MPI_COMM_WORLD);
MPI_Pack(&j, 1, MPI_INT, buf, 1000, &position, MPI_COMM_WORLD);
MPI_Send(buf, position, MPI_PACKED, 1, 99, MPI_COMM_WORLD);
}
else if (myid == 1){
MPI_Recv(k, 2, MPI_INT, 0, 99, MPI_COMM_WORLD, &status);
position = 0 ;
i = j = 0;
// your code here
MPI_Unpack(k, 2, &position, &i, 1, MPI_INT, MPI_COMM_WORLD);
MPI_Unpack(k, 2, &position, &j, 1, MPI_INT, MPI_COMM_WORLD);
// end of your code
printf("The number is %d and %d", i, j);
}
MPI_Finalize();
return 0;
}