项目介绍
- 使用
MPI
实现多进程并行快速排序
MPI简介
-
什么是MPI
与
OpenMP
并行程序不同,MPI
是一种基于信息传递的并行编程技术。消息传递接口是一种编程接口标准,而不是一种具体的编程语言。简而言之,MPI
标准定义了一组具有可移植性的编程接口- Point-to-point communication
- Collective operations
- Process groups, Communication contexts, and Process topologies
- Environmental management and inquiry
- Process creation and management
- Other functions
-
MPI组成
-
数据类型
定义了精确的数据类型参数而不使用字节计数,以数据类型为单位指定消息的长度;对于
C
和Fortran
,MPI
均预定义了一组数据类型和一些附加的数据类型;可以发送 或接收连续的数据,还可以处理不连续的数据;允许发送和接收不同的数据类型 -
通信域
MPI
预定义的通信域:mpi comm world
(包含所有进程)、mpi comm self
(只包含各个进程自己的进程组)
-
-
编程详解
#include "mpi.h" //Header File int main (int argc, char *argv[]){ MPI_Init (&argc, &argv); //Initialization MPI_Comm_size (COMM, &p);//Set the number of processes MPI_Comm_rank (COMM, &id);//id represents the current process number /* Communicate & Compute; */ MPI_Finalize( ); return 0; }
-
MPI 基本函数
-
Getting Communicator Information
//Get the rank of the calling process in group int MPI_Comm_rank(MPI_Comm comm, int *rank); //Get the size in a communicator int MPI_Comm_size(MPI_Comm comm, int *size);
例子:
#include <stdio.h> #include "mpi.h" int main(int argc, char**argv){ MPI_Init(&argc, &argv); printf("Hello world.\n"); MPI_Finalize(); return 0; }
运行:
$mpicc basic.c -o basic //编译 $mpirun -np 3 ./basic > basic.txt //设置进程数为3,linux重定向结果输出到basic.txt
输出:
Hello world. Hello world. Hello world.
-
Point to Point communication
//发送消息 Send a message MPI_Send( void* data, //starting address of the data to be sent int count, //number of elements to be sent (not bytes) MPI_Datatype datatype, //MPI datatype of each element int destination, //rank of destination process int tag, //message identifier (set by user) MPI_Comm comm //MPI communicator of processors involved )
//接受消息 Receive a message MPI_Recv( void* data, //starting address of buffer to store message int count, //number of elements to be received (not bytes) MPI_Datatype datatype, //MPI datatype of each element int source, //rank of source process int tag, //message identifier (set by user) MPI_Comm comm, //MPI communicator of processors involved MPI_Status* status //structure of information about the message )
例子:
int main(int argc, char **argv){ MPI_Comm comm = MPI_COMM_WORLD; MPI_Status status; int size, rank; char str[100]; MPI_Init(&argc, &argv); MPI_Comm_size(comm, &size); MPI_Comm_rank(comm, &rank); if (rank == 0) { strcpy(str, "hello world"); printf("Process 0 send 1 to process %s.\n", str); MPI_Send(str, strlen(str) + 1, MPI_CHAR, 1, 99, comm); } else if (rank == 1) { MPI_Recv(str, 100, MPI_CHAR, 0, 99, comm, &status); printf("Process 1 receives messages %s.\n", str); } MPI_Finalize(); return 0; }
输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nrk1govP-1627008119456)(C:\Users\86176\AppData\Roaming\Typora\typora-user-images\image-20210722171227625.png)]
-
Collective Communication
Broadcast
copies data from the memory of one processor to that of other processors —— One to all operation[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HDfXO1w7-1627008119459)(C:\Users\86176\AppData\Roaming\Typora\typora-user-images\image-20210722171458642.png)]
int MPI_Bcast( void* buffer, //starting address of buffer int count, //number of entries in buffer MPI_Datatype datatype, //data type of buffer int root, //rank of broadcast root MPI_Comm comm //communicator )
例子:
int main(int argc, char** argv){ int arr[3], i, rank; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &rank); if (rank == 0){ for (i = 0; i < 3; i++) arr[i] = i + 1; } MPI_Bcast(arr, 3, MPI_INT, 0, MPI_COMM_WORLD); printf("Process %d receives:", rank); for (i = 0; i < 3; i++) printf("%d ", arr[i]); putchar('\n'); MPI_Finalize(); return 0; }
输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kG1yOWLP-1627008119460)(C:\Users\86176\AppData\Roaming\Typora\typora-user-images\image-20210722171945635.png)]
Gather
copies data from each process to one process, where it is stored in rank order—— One to all operation[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dQEz0ejE-1627008119461)(C:\Users\86176\AppData\Roaming\Typora\typora-user-images\image-20210722172032364.png)]
-
int MPI_Gather(
const void* sendbuf, //starting address of send buffer
int sendcount, //number of elements in send buffer
MPI_Datatype sendtype, //data type of send buffer elements
void* recvbuf, //address of receive buffer (significant only at root)
int recvcount, //number of elements for any single receive (significant only at root)
MPI_Datatype recvtype, //data type of recv buffer elements(significant only at root)
int root, //rank of receiving process
MPI_Comm comm //communicator
)
例子:
int main(int argc, char **argv){
int rank, size, sbuf[3], *rbuf, i;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
for (i = 0; i < 3; i++) sbuf[i] = rank * 10 + i;
if (rank == 0) rbuf = (int*)malloc(sizeof(int) * 3 * size);
MPI_Gather(sbuf, 3, MPI_INT, rbuf, 3, MPI_INT, 0, MPI_COMM_WORLD);
if (rank == 0){
printf("Process 0 receives:");
for (i = 0; i < size * 3; i++)
printf("%d ", rbuf[i]);
putchar('\n');
}
MPI_Finalize();
return 0;
}
输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d4CZEcPn-1627008119462)(C:\Users\86176\AppData\Roaming\Typora\typora-user-images\image-20210722172256838.png)]
MPI实现快排
设计思路
- 利用多进程设计进行并行快速排序,需要
log2(n)
个进程 - 其中0号进程进行初始分块,调用
partition()
函数将数组分成两部分 - 将后一半调用
MPI_Send()
将数据发送到2^n-1
线程; - 随后将进程分成对应的发送进程和接收进程两部分
- 最后,对每个进程进行串行快速排序;
- 随后从各进程收集结果,完成并行排序。
自定义的库
-
为了方便(
MPI
,openMP
和MPI
混合)对于相同函数的调用,编写通用函数库quickSort.h
原码:
#include "omp.h" //随机创建数组 void rands(int *data,int sum); //交换函数 void sw(int *a, int *b); //求2的n次幂 int exp2(int wht_num); //求log2(n) int log2(int wht_num); //合并两个有序的数组 void mergeList(int *c,int *a, int sta1, int end1, int *b, int sta2, int end2); //串行快速排序 int partition(int *a, int sta, int end); void quickSort(int *a, int sta, int end); //openMP(8)并行快速排序 void quickSort_parallel(int* array, int lenArray, int numThreads); void quickSort_parallel_internal(int* array, int left, int right, int cutoff); void rands(int *data, int sum){ int i; for (i = 0; i < sum; i++) { data[i] = rand() % 100000000; } } void sw(int *a, int *b){ int t = *a; *a = *b; *b = t; } int exp2(int wht_num){ int wht_i; wht_i=1; while(wht_num>0) { wht_num--; wht_i=wht_i*2; } return wht_i; } int log2(int wht_num){ int wht_i, wht_j; wht_i=1; wht_j=2; while(wht_j<wht_num) { wht_j=wht_j*2; wht_i++; } if(wht_j>wht_num) wht_i--; return wht_i; } int partition(int *a, int sta, int end){ int i = sta, j = end + 1; int x = a[sta]; while (1) { while (a[++i] < x && i < end); while (a[--j] > x); if (i >= j) break; sw(&a[i], &a[j]); } a[sta] = a[j]; a[j] = x; return j; } //并行openMP排序 void quickSort_parallel(int *a, int lenArray, int numThreads){ int cutoff = 1000; #pragma omp parallel num_threads(numThreads) //指定线程数的数量 { #pragma omp single //串行执行 { quickSort_parallel_internal(a, 0, lenArray - 1, cutoff); } } } void quickSort_parallel_internal(int *a, int left, int right, int cutoff){ int i = left, j = right; int tmp; int pivot = a[(left + right) / 2]; //进行数组分割,分成两部分(符合左小右大) while (i <= j) { while (a[i] < pivot) i++; while (a[j] > pivot) j--; if (i <= j){ tmp = a[i]; a[i] = a[j]; a[j] = tmp; i++; j--; } } //int j = partition(a,left,right); if (((right - left) < cutoff)){ if (left < j){ quickSort_parallel_internal(a, left, j, cutoff); } if (i < right){ quickSort_parallel_internal(a, i, right, cutoff); } } else{ #pragma omp task /* task是“动态”定义任务的,在运行过程中, 只需要使用task就会定义一个任务, 任务就会在一个线程上去执行,那么其它的任务就可以并行的执行。 可能某一个任务执行了一半的时候,或者甚至要执行完的时候, 程序可以去创建第二个任务,任务在一个线程上去执行,一个动态的过程 */ //对两部分再进行并行的线程排序 { quickSort_parallel_internal(a, left, j, cutoff); } #pragma omp task { quickSort_parallel_internal(a, i, right, cutoff); } } } //合并两个已排序的数组 void mergeList(int *c,int *a, int sta1, int end1, int *b, int sta2, int end2){ int a_index = sta1; // 遍历数组a的下标 int b_index = sta2; // 遍历数组b的下标 int i = 0; // 记录当前存储位置 //int *c; //c = (int *)malloc(sizeof(int) * (end1 - sta1 + 1 + end2 - sta2 + 1)); while (a_index < end1 && b_index < end2){ if (a[a_index] <= b[b_index]){ c[i] = a[a_index]; a_index++; } else{ c[i] = b[b_index]; b_index++; } i++; } while (a_index < end1){ c[i] = a[a_index]; i++; a_index++; } while (b_index < end2){ c[i] = b[b_index]; i++; b_index++; } } //串行快速排序 void quickSort(int *a, int sta, int end){ if (sta < end){ //printf("3\n"); int mid = partition(a, sta, end); quickSort(a, sta, mid - 1); quickSort(a, mid + 1, end); } }
-
MPI实现快速排序
#include <stdio.h> #include "mpi.h" #include <time.h> #include <stdlib.h> #include "quickSort.h" #define n 10000000 double whi_sta,whi_end; //并行排序 void para_quickSort(int *data,int sta,int end,int whi_m,int id,int now_id){ //printf("1\n"); int whi_r,whi_j,whi_i; int MyLength = -1; int *tmp; MPI_Status status; //可剩处理器为0,进行串行快速排序排序 if(whi_m == 0){ whi_sta = MPI_Wtime(); if(now_id == id){ quickSort(data,sta,end); } return ; } //由当前处理器进行分块 if(now_id == id){ whi_r = partition(data,sta,end); MyLength = end - whi_r; MPI_Send(&MyLength,1,MPI_INT,id+exp2(whi_m-1),now_id,MPI_COMM_WORLD); if(MyLength != 0){ MPI_Send(data+whi_r+1,MyLength,MPI_INT,id+exp2(whi_m1),now_id,MPI_COMM_WORLD); } } if(now_id == id+exp2(whi_m-1)){ MPI_Recv(&MyLength,1,MPI_INT,id,id,MPI_COMM_WORLD,&status); if(MyLength != 0){ tmp = (int*)malloc(sizeof(int)*MyLength); MPI_Recv(tmp,MyLength,MPI_INT,id,id,MPI_COMM_WORLD,&status); } } whi_j = whi_r-1-sta; MPI_Bcast(&whi_j,1,MPI_INT,id,MPI_COMM_WORLD); if(whi_j > 0){ para_quickSort(data,sta,whi_r-1,whi_m-1,id,now_id); } whi_j = MyLength; MPI_Bcast(&whi_j,1,MPI_INT,id,MPI_COMM_WORLD); if(whi_j > 0){ para_quickSort(tmp,0,MyLength-1,whi_m-1,id+exp2(whi_m-1),now_id); } if(now_id == id+exp2(whi_m-1)&&MyLength != 0){ MPI_Send(tmp,MyLength,MPI_INT,id,id+exp2(whi_m-1),MPI_COMM_WORLD); } if((now_id == id) && (MyLength != 0)) MPI_Recv(data+whi_r+1,MyLength,MPI_INT,id+exp2(whi_m-1),id+exp2(whi_m-1),MPI_COMM_WORLD,&status); } int main(int argc,char *argv[]) { int *data1,*data2; int now_id,sum_id; int whi_m,whi_r; int whi_i,whi_j; MPI_Status status; //启动mpi MPI_Init(&argc,&argv); //确定自己的进程标志符now_id MPI_Comm_rank(MPI_COMM_WORLD,&now_id); //组内进程数是sum_id MPI_Comm_size(MPI_COMM_WORLD,&sum_id); double whi_n; if(now_id == 0){ //初始化串行和并行所需排序的随机数组 whi_n = n; data1 = (int*)malloc(sizeof(int)*whi_n); data2 = (int*)malloc(sizeof(int)*whi_n); rands(data1,n); rands(data2,n); //打印初始数组 //printf("---------\n"); // printf("The original array data1: \n"); //print(data1,n); //printf("The original array data2: \n"); // print(data2,n); //printf("---------\n"); } whi_m = log2(sum_id); MPI_Bcast(&whi_n,1,MPI_INT,0,MPI_COMM_WORLD); //并行排序 para_quickSort(data1,0,whi_n-1,whi_m,0,now_id); whi_end = MPI_Wtime(); if(now_id == 0){ double sta_time2 = MPI_Wtime(); quickSort(data2,0,n-1); double end_time2 = MPI_Wtime(); //printf("----------\n"); // printf("The final array data1 : \n"); //print(data1,n); //printf("----------\n"); printf("串行时间 = %f s\n",end_time2-sta_time2); printf("并行时间 = %f s\n",whi_end-whi_sta); } MPI_Finalize(); return 0; }
-
性能分析
实际测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BCzCxaOi-1627008119463)(C:\Users\86176\AppData\Roaming\Typora\typora-user-images\image-20210723101736425.png)]
CPU
串行快排随数据规模N
增大而呈指数递增,而MPI
并行快排随数据规模N
增大而呈线性递增。