MPI并行化桶排序
- 桶排序(Bucket Sort)
- 并行化桶排序算法
- 简单的方法
- 更好的方法
- 代码实现
本文主要讲述了并行桶排序算法,以及如何使用MPI集合通信来实现。
桶排序(Bucket Sort)
桶排序假定待排序数据 [ a 0 , a 1 , ⋯ , a n − 1 ] [a_0,a_1,\cdots, a_{n-1}] [a0,a1,⋯,an−1] 的任何一个元素都满足 MIN ≤ a i ≤ MAX \text{MIN} \leq a_i \leq \text{MAX} MIN≤ai≤MAX,其中MIN和MAX是常数,即所有的元素都落在相同的固定范围内。
桶排序把范围
[
MIN
,
MAX
]
[\text{MIN}, \text{MAX}]
[MIN,MAX] 划分为若干个子范围,每个范围对应一个 桶
,比如说可以把
[
0
,
10
]
[0,10]
[0,10] 划分为
[
0
,
5
]
[0,5]
[0,5] 和
(
5
,
10
]
(5,10]
(5,10] 这两个子区间。
接下来遍历待排序的数据,根据当前元素的值,将其划分到对应的桶中去。划分完成后,对每个桶中的元素单独进行排序(使用其他排序算法完成这个排序),最终再把每个桶的数据合并,即可完成对所有数据的排序。
下面看一个例子,假设待排序数据为 [ 9 , 3 , 8 , 4 ] [9,3,8,4] [9,3,8,4],每个元素的都在 [ 0 , 10 ] [0,10] [0,10] 内。假设将 [ 0 , 10 ] [0,10] [0,10] 划分两个桶 [ 0 , 5 ] [0,5] [0,5] 和 [ 6 , 10 ] [6,10] [6,10],分别为 B [ 0 ] B[0] B[0] 和 B [ 1 ] B[1] B[1]
- 遍历待排序的数据,根据当前元素的值,将其划分到对应的
桶
中去:这一步完成后, B [ 0 ] = { 3 , 4 } B[0]=\{3,4\} B[0]={3,4}, B [ 1 ] = { 9 , 8 } B[1]=\{9,8\} B[1]={9,8}- 对每个桶中的元素单独进行排序(使用其他排序算法完成这个排序):这一步完成后, B [ 0 ] = { 3 , 4 } B[0]=\{3,4\} B[0]={3,4}, B [ 1 ] = { 8 , 9 } B[1]=\{8,9\} B[1]={8,9}
- 把每个桶的数据合并:依次合并 B [ 0 ] , B [ 1 ] B[0],B[1] B[0],B[1],最终得到排序后的数据 { 3 , 4 , 8 , 9 } \{3,4,8,9\} {3,4,8,9}
并行化桶排序算法
简单的方法
一个最简单的思路就是,每个进程对应一个桶,比如说一共有8个进程,那就把范围分为8个子范围(桶),每个进程负责一个桶,如下所图所示。
这个并行算法不足之处在于,如果待排序的数据不是均匀的分布在 [ MIN , MAX ] [\text{MIN},\text{MAX}] [MIN,MAX] 上,那么可能存在一些进程,它们负责的桶里面可能数据很少甚至根本没有,而部分进程负责桶里的数据又会过多,这会使得任务划分不均衡;此外最后合并的
更好的方法
现在描述另外一种并行化的思路,假设进程数为 n n n,并且将区间 [ MIN , MAX ] [\text{MIN}, \text{MAX}] [MIN,MAX] 划分为 n n n 个桶,分别为 B [ 0 ] , B [ 1 ] , ⋯ , B [ n − 1 ] B[0],B[1],\cdots,B[n-1] B[0],B[1],⋯,B[n−1]
- 首先将待排序数据平均划分到各个进程上
- 每个进程使用上面划分的 n n n 个桶,把它自己的数据划分到各个桶中
- 接下来进程间进行通信,每个进程都需要把自己的桶 Local_B [ i ] \text{Local\_B}[i] Local_B[i] 里的数据发送给进程 i i i,假设桶的编号依次为 0 , 1 , ⋯ , n − 1 0,1,\cdots,n-1 0,1,⋯,n−1,进程的编号依次为 0 , 1 , ⋯ , n − 1 0,1,\cdots,n-1 0,1,⋯,n−1。这一步完成后,全部待排序的数据已经成功地划分到各个桶中了,并且进程 i i i 拥有 Global_B[i] \text{Global\_B[i]} Global_B[i] 的数据,但是此时 Global_B[i] \text{Global\_B[i]} Global_B[i]中的数据并没有排好序
- 每个进程对他自己负责的桶进行排序,这一步需要使用其他排序算法完成
- 最后再将各个进程桶中的数据汇总即可
实现进程间的通信,需要MPI中相关函数的支持。第一步可以使用 MPI_Scatter()
实现;第三步可以使用 MPI_Alltoallv()
实现;第五步可以使用 MPI_Gatherv()
实现。MPI_Scatter()
和 MPI_Gatherv()
原理较为简单,这里不再赘述,下面主要讲一下 MPI_Alltoall()
的通信过程,MPI_Alltoallv()
是类似的。
MPI_Alltoall()
主要是用于将每个进程的数据发送到所有的进程,比如说假设一共
n
n
n 个进程,每个进程都对自己的数组
Local_Data
\text{Local\_Data}
Local_Data 调用 MPI_Alltoall()
,那么进程
i
i
i 会首先把自己的
Local_Data
\text{Local\_Data}
Local_Data 分成
n
n
n 份,然后分别发一份到每一个进程(把第
j
j
j 份发送给进程
j
j
j)。因此完成通信后,每个进程会收到
n
n
n 份来自其他进程的数据。
实际上,MPI_Alltoall()
也可以这么理解, “all-to-all” routine actually transfers rows of an array to columns. 下图是一个示例来帮助理解。
可以看到,通信前数组 A A A 按照行均匀地划分到各个进程,通信后,数组 A A A 则按照列均匀地划分到各个进程
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
#include <sys/time.h>
#include <time.h>
// #define PRINT_RES
// return the difference between end and start in microseconds
int timeDiff(struct timeval start, struct timeval end) {
return (end.tv_sec-start.tv_sec)*1000000 + (end.tv_usec-start.tv_usec);
}
// The compar for qsort
int cmpfunc(const void *a, const void *b){
return (*(int*)a - *(int*)b);
}
void print_arr(int* arr, int len){
int i;
for(i = 0; i < len; ++i){
printf("%d ", arr[i]);
}
printf("\n");
}
int main(int argc, char *argv[]){
int my_rank, comm_sz;
int n, i, local_n;
struct timeval start_time, end_time;
long local_runtime, total_runtime;
int *total_array, *local_array, num_buckets;
int *local_small_buckets, *local_large_bucket;
int *len_local_small_buckets, len_local_large_bucket;
int *sdispls, *recvcounts, *rdispls;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);
sdispls = malloc(sizeof(int) * comm_sz);
recvcounts = malloc(sizeof(int) * comm_sz);
rdispls = malloc(sizeof(int) * comm_sz);
num_buckets = comm_sz; // The number of buckets is equal to the number of processes
// get the size of array from the command
if (argc < 2) {
n = 10000;
} else {
n = atoi(argv[1]);
}
local_n = n / comm_sz;
if(local_n <= 0){
local_n = 1;
}
n = local_n * comm_sz;
if(my_rank == 0){
srand(0);
total_array = (int*) malloc(sizeof(int) * n);
local_array = (int*) malloc(sizeof(int) * local_n);
for (i = 0; i < n; ++i){
total_array[i] = rand();
}
}else{
local_array = (int*) malloc(sizeof(int) * local_n);
}
gettimeofday(&start_time, NULL);
// The master needs to partition total_array and send each partition to the correct slave process
MPI_Scatter(total_array, local_n, MPI_INT, local_array, local_n, MPI_INT, 0, MPI_COMM_WORLD);
// Small bucket distribution
local_small_buckets = malloc(sizeof(int*) * local_n * num_buckets);
len_local_small_buckets = calloc(num_buckets, sizeof(int));
int tmp = RAND_MAX / num_buckets + 1;
for (i = 0; i < local_n; ++i){
int idx = local_array[i] / tmp;
local_small_buckets[local_n * idx + len_local_small_buckets[idx]++] = local_array[i];
}
// Small buckets then emptied into p final buckets for sorting
local_large_bucket = malloc(sizeof(int) * n);
len_local_large_bucket = 0;
for (i = 0; i < comm_sz; ++i){
sdispls[i] = local_n * i;
}
MPI_Alltoall(len_local_small_buckets, 1, MPI_INT, recvcounts, 1, MPI_INT, MPI_COMM_WORLD);
rdispls[0] = 0;
len_local_large_bucket = recvcounts[0];
for (i = 1; i < comm_sz; ++i){
rdispls[i] = rdispls[i-1] + recvcounts[i-1];
len_local_large_bucket += recvcounts[i];
}
MPI_Alltoallv(local_small_buckets, len_local_small_buckets, sdispls, MPI_INT, local_large_bucket, recvcounts, rdispls, MPI_INT, MPI_COMM_WORLD);
// Sort the local_large_bucket
qsort(local_large_bucket, len_local_large_bucket, sizeof(int), cmpfunc);
gettimeofday(&end_time, NULL);
local_runtime = timeDiff(start_time, end_time);
MPI_Reduce(&local_runtime, &total_runtime, 1, MPI_LONG, MPI_MAX, 0, MPI_COMM_WORLD);
// Gather all the data to the main process
MPI_Gather(&len_local_large_bucket, 1, MPI_INT, recvcounts, 1, MPI_INT, 0, MPI_COMM_WORLD);
if(my_rank == 0){
rdispls[0] = 0;
for (i = 1; i < comm_sz; ++i){
rdispls[i] = rdispls[i-1] + recvcounts[i-1];
}
}
MPI_Gatherv(local_large_bucket, len_local_large_bucket, MPI_INT, total_array, recvcounts, rdispls, MPI_INT, 0, MPI_COMM_WORLD);
if (my_rank == 0){
int is_sorted = 1;
for(i = 1; i < n; ++i){
if (total_array[i] < total_array[i-1]){
is_sorted = 0;
break;
}
}
if(is_sorted){
printf("The Data has been sorted.\n");
}else{
printf("Data Not Sorted in index %d with %d > %d\n", i, total_array[i-1], total_array[i]);
}
#ifdef PRINT_RES
print_arr(total_array, n);
#endif
printf("Total runtime (in microseconds): %ld\n", total_runtime);
free(total_array);
}
free(local_array);
free(len_local_small_buckets);
free(local_small_buckets);
free(local_large_bucket);
free(rdispls);
free(recvcounts);
free(sdispls);
MPI_Finalize();
return 0;
}