快速排序算法的效率相对较高,并行算法在理想的情况下时间复杂度可达到o(n),但并行快速排序算法有一个严重的问题:会造成严重的负载不平衡,最差情况下算法的复杂度可达o(n^2)。本篇我们介绍一种基于均匀划分的负载平衡的并行排序算法------并行正则采样排序(Parallel Sorting by Regular Sampling)。
一、算法的基本思想
假设待排序的元素n个,处理器p个。
首先将这n个元素均匀的分成p部分,每部分包含n/p个元素。每个处理器负责其中的一部分,并对其进行局部排序。为确定局部有序序列在整个序列中的位置,每个处理器从各自的局部有序序列中选取几个代表元素,将这些代表元素进行排序后选出p-1个主元。每个处理器根据这p-1个主元将自己的局部有序序列分成p段。然后通过全局交换的方式,将p段有序序列分发给对应的处理器,使第i个处理器都拥有各个处理器的第i段,共p段有序序列。每个处理器对着p段有序序列进行排序。最后,将各个处理器的有序段依次汇合起来,就是全局有序序列了。
二、算法描述
根据算法的基本思想,我们对算法的描述如下:
输入:n个待排序的序列
输出:分布在各个处理器上,得到全局有序的数据序列
1)无序序列的划分及局部排序
根据数据快的划分方法(请看系列一),将无序序列划分成p部分,每个处理器对其中的一部分进行串行快速排序,这样每个处理器就会拥有一个局部有序序列。
2)选取代表元素
每个处理器从局部有序序列中选取第w,2w,...,(p-1)w共p-1个代表元素。其中w = n/p^2。
3)确定主元
每个处理器都将自己选取好的代表元素发送给处理器p0。p0对这p段有序序列做多路归并排序,再从这排序后的序列中选取第p-1,2(p-1), ...,(p-1)(p-1)共p-1个元素作为主元。
4)分发主元
p0将这p-1个主元分发给各个处理器。
5)局部有序序列划分
每个处理器在接收到主元后,根据主元将自己的局部有序序列划分成p段。
6)p段有序序列的分发
每个处理器将自己的第i段发送给第i个处理器,是处理器i都拥有所有处理器的第i段。
7)多路排序
每个处理器将上一步得到的p段有序序列做多路归并。
经过这7步后,一次将每个处理器的数据取出,这些数据是有序的。
三、算法分析
1)负载均衡分析:
因为这个算法是一个负载平衡的算法,者从第1)步中就可以看出来,但却不是完美的,因为在第6)步的划分很可能会引起负载的不平衡。
2)时间复杂度分析
PSRS算法适合处理大批量的数据(呵呵,数据量不大,何必并行乎)。当n>p^3时,算法的时间复杂度可达n/p*logn。具体每一步的时间复杂度的分析在这里就不一一描述了,因为每一步的排序都是普通的串行排序算法。
四、算法实现
因为算法比较复杂,代码较长,本文仅仅列出主代码,代码如下:
1: void psrs_mpi(int *argc, char ***argv){
2: 3: int process_id;
4: int process_size;
5: 6: int *init_array; //初始数组
7: int init_array_length; //初始数组长度
8: 9: int *local_sample; //每个进程选取的代表元素数组
10: int local_sample_length; //代表元素数组长度
11: 12: int *sample; //代表元素集合(0号进程使用)
13: int *sorted_sample; //排序后的代表元素的集合
14: int sample_length; //代表预算的长度
15: 16: int *primary_sample; //主元
17: 18: int *resp_array; //偏移数组,主要用户指定个进程数组的各分段的长度
19: 20: int *section_resp_array; //偏移数组,用于指定进程从其他进程获得的数组的长度
21: 22: int *section_array; //从各个进程中获得分段数组的集合
23: int *sorted_section_array;
24: int section_array_length; //总长
25: 26: int section_index;
27: 28: int i, j ; //循环变量
29: 30: MPI_Request handle; 31: MPI_Status status; 32: 33: mpi_start(argc, argv, &process_size, &process_id, MPI_COMM_WORLD);34: resp_array = (int *)my_mpi_malloc(process_id, sizeof(int) * process_size);
35: 36: //为每个进程构建一个数组
37: //并对改进型的数组进行串行快速排序
38: init_array_length = ARRAY_LENGTH;39: init_array = (int *)my_mpi_malloc(process_id, sizeof(int) * init_array_length);
40: array_builder_seed(init_array, init_array_length, process_id); 41: 42: quick_sort(init_array, 0, init_array_length -1); 43: 44: //每个处理器从排序号的序列中选取process_size-1个元素
45: //并发送到0号进程中
46: local_sample_length = process_size - 1; 47: local_sample = array_sample(init_array, local_sample_length, init_array_length/process_size, process_id); 48: 49: if(process_id)
50: MPI_Send(local_sample, local_sample_length, MPI_INT, 0, SAMPLE_DATA, MPI_COMM_WORLD); 51: 52: 53: //0号进程接收各处理器发送过来的代表元素,并将这些元素做多路归并排序
54: if(!process_id){
55: sample = (int *)my_mpi_malloc(0, sizeof(int) * process_size * local_sample_length);
56: sorted_sample = (int *)my_mpi_malloc(0, sizeof(int) * process_size * local_sample_length);
57: array_copy(sample, local_sample, local_sample_length); 58: 59: for(i = 1; i < process_size; i++)
60: MPI_Irecv(sample + local_sample_length * i, local_sample_length, MPI_INT, i, SAMPLE_DATA, 61: MPI_COMM_WORLD, &handle); 62: 63: MPI_Wait(&handle, &status); 64: 65: for(i = 0; i < process_size; i++)
66: resp_array[i] = local_sample_length; 67: 68: mul_merger(sample, sorted_sample, resp_array, process_size); 69: 70: //从排序好的代表元素中选取process_size-1个主元,并将这些主元广播道其他的处理器中
71: primary_sample = array_sample(sorted_sample, process_size -1, process_size -1, process_id); 72: }73: if(process_id)
74: primary_sample = (int *)my_mpi_malloc(process_id, sizeof(int) * process_size -1);
75: 76: MPI_Bcast(primary_sample, process_size-1, MPI_INT, 0, MPI_COMM_WORLD); 77: 78: //将处理器上的数据根据主元分成process_size 端
79: get_array_sepator_resp(init_array, primary_sample, resp_array, init_array_length, process_size); 80: if(process_id == ID){
81: printf("process %d resp array is:" ,process_id);
82: array_int_print(process_size, resp_array); 83: } 84: 85: //每个处理器将自己的第i段发送给第i个处理器
86: section_resp_array = (int *)my_mpi_malloc(process_id, sizeof(int) * process_size);
87: section_resp_array[process_id] = resp_array[process_id]; 88: 89: //每个进程将要发送的数据的个数发送给哥哥处理器
90: for(i = 0; i < process_size; i++){
91: if(i == process_id){
92: for(j = 0; j < process_size; j++)
93: if(i != j)
94: MPI_Send(&(resp_array[j]), 1, MPI_INT, j, SECTION_INDEX , 95: MPI_COMM_WORLD); 96: }97: else
98: MPI_Recv(&(section_resp_array[i]), 1, MPI_INT, i, SECTION_INDEX, 99: MPI_COMM_WORLD, &status); 100: } 101: 102: MPI_Barrier(MPI_COMM_WORLD); 103: 104: section_array_length = get_array_element_total(section_resp_array, 0, process_size - 1);105: section_array = (int *)my_mpi_malloc(process_id, sizeof(int) * section_array_length);
106: sorted_section_array = (int *)my_mpi_malloc(process_id, sizeof(int) * section_array_length);
107: section_index = 0; 108: 109: for(i = 0; i < process_size; i++){
110: if(i == process_id){
111: for(j = 0; j < process_size; j++){
112: if(j)
113: section_index = get_array_element_total(resp_array, 0 , j-1);114: if(i == j)
115: array_int_copy(section_array, init_array, section_index, section_index+resp_array[j]);116: if(i != j){
117: if(j)
118: section_index = get_array_element_total(resp_array, 0 , j-1); 119: MPI_Send(&(init_array[section_index]), resp_array[j], MPI_INT, 120: j, SECTION_DATA, MPI_COMM_WORLD); 121: } 122: } 123: }124: else{
125: if(i)
126: section_index = get_array_element_total(section_resp_array, 0, i-1); 127: MPI_Recv(&(section_array[section_index]), section_resp_array[i], MPI_INT, 128: i, SECTION_DATA, MPI_COMM_WORLD, &status); 129: } 130: } 131: MPI_Barrier(MPI_COMM_WORLD); 132: 133: //进行多路归并排序
134: mul_merger(section_array, sorted_section_array, section_resp_array, process_size); 135: 136: array_int_print(section_array_length, sorted_section_array); 137: 138: //释放内存
139: free(resp_array); 140: free(init_array); 141: free(local_sample); 142: free(primary_sample); 143: free(section_array); 144: free(sorted_section_array); 145: free(section_resp_array); 146: 147: if(!process_id){
148: free(sample); 149: free(sorted_sample); 150: } 151: 152: MPI_Finalize(); 153: }五、MPI函数分析
在上述算法中,用到了MPI的非阻塞通信函数:MPI_IRecv,其对应的是MPI_Isend。这连个函数用于进程间的非阻塞通信,使通信和运算能够同时进行。有这两
个非阻塞通信函数,就不能不提MPI_Wait函数,该函数的作用是阻塞进程执行,直到想对应的所有进程操作都执行的这个地方为止。一般是这个三函数一起使用。
下篇,我们将介绍kmp字符串匹配算法及其并行化。
浙公网安备 33010602011771号