目录
算法介绍
冒泡排序:是串行算法,在每次迭代过程中,每个元素可能与前面元素交换,也可能和后面的元素交换,数据的相关性比较强很难直接改成并行算法。
奇偶排序:或奇偶换位排序,或砖排序,是一种相对简单的排序算法,最初发明用于有本地互连的并行计算。这是与冒泡排序特点类似的一种比较排序。该算法中,排序过程分两个阶段,奇交换和偶交换,两种交换都是成对出现。对于奇交换,它总是比较奇数索引以及其相邻的后续元素。对偶交换,总是比较偶数索引和其相邻的后续元素。
可看出整个比较交换分为独立的奇阶段或偶阶段。在每个阶段内,所有的比较和交换没有数据相关性。因此,每一次比较和交换都可独立执行,也就可以并行化。
算法实现
import copy
import random
import time
import numpy as np
from mpi4py import MPI
N = 10000 # 待排元素个数
INF = 999999999 # 设置一个无穷大值
masterNode = 0 # 设置主进程标号
global myid # 进程标号
global part # 每个进程分配的元素个数
global sort # 用来表示排序是否完成的逻辑量
局部排序
这个阶段可以使用任意传统的排序算法,如冒泡排序,快速排序,局部奇偶排序等等,本文使用最简单的冒泡排序。
def odd_even_sort(array, num): # 冒泡排序
for i in range(num - 1):
isNotchange = True
for j in range(num - i - 1):
# print(array[j], j)
if array[j] > array[j + 1]:
array[j], array[j + 1] = array[j + 1], array[j]
isNotchange = False
if isNotchange == True:
break
# def odd_even_sort1(p, num): # 局部奇偶排序
# sort = 0
# while not sort: # 一次奇数排序和一次偶数排序均未发生数据交换时排序完成
# sort = 1
# for i in range(1, num, 2):
# if p[i] < p[i - 1]:
# swap(p, i, i - 1)
# sort = 0
# for i in range(2, num, 2):
# if p[i] < p[i - 1]:
# swap(p, i, i - 1)
# sort = 0
排序合并
def mergeSort(data, buffer, tmp): # 两块排好序的元素data和buffer进行排序合并
i = 0
j = 0
k = 0
while i != part and j != part:
if data[i] <= buffer[j]:
tmp[k] = data[i]
k = k + 1
i = i + 1
else:
tmp[k] = buffer[j]
k = k + 1
j = j + 1
if j != 0:
sort = 0
if i != part:
while k != 2 * part:
tmp[k] = data[i]
k = k + 1
i = i + 1
if j != part:
while k != 2 * part:
tmp[k] = buffer[j]
k = k + 1
j = j + 1
data = copy.deepcopy(tmp)
buffer = copy.deepcopy(tmp + part)
奇偶融合
值得注意的是,程序实际运行时,每个进程存储的数据(data)并不是像文章开头的示意图一样,只保存固定个数的数据,而是随着奇偶融合的不断进行,逐渐包括进程编号相邻的进程数据。比如图中的1号进程,在一次奇偶融合排序后,实际上存储了0、1、2、3号进程的有序数据。
存在严格的定理可证明,如果有n个数进行奇偶排序,至多进行n次排序,数据则有序;对于MPI并行算法,类比可得,如果有n个进程进行奇偶排序,则同样至多进行n次排序(一次排序指的是一次奇排序加一次偶排序)
但事实上,可设置标志位,若一次排序中,没有发生数据交换,则可以跳出循环,不用执行n步,本文的程序实现正是这种思路。
def parallelMergeSort(data): # 奇偶融合
left = myid - 1
right = myid + 1
# 边缘进程的特殊处理
if left < 0:
left = None
if right > numprocs - 1:
right = None
# buffer存放即将与本地data合并的数据
buffer = np.empty(part)
# tmp存放buffer和data合并后的数据,
# 实际上也可以在mergeSort函数内创建,
# 在此处创建tmp是出于程序性能的考虑
tmp = np.empty(part * 2)
sort = 0
flag = 0
# 奇偶融合重复多次直到所有合并都不需要进行排序
while not flag:
sort = 1
# 奇排序
if myid % 2 == 1:
comm.send(data, dest=left)
data = comm.recv(source=left)
elif right is not None:
buffer = comm.recv(source=right)
mergeSort(data, buffer, tmp)
comm.send(buffer, dest=right)
# 偶排序
if myid % 2 == 0 and myid != 0:
comm.send(data, dest=left)
data = comm.recv(source=left)
elif right is not None and myid != 0:
buffer = comm.recv(source=right)
mergeSort(data, buffer, tmp)
comm.send(buffer, dest=right)
# 全规约函数,对每个进程中的sort标识求逻辑与,结果保存在flag
# flag为真表明全局有序
flag = comm.allreduce(sort, op=MPI.LAND)
主程序
if __name__ == '__main__':
comm = MPI.COMM_WORLD
myid = comm.Get_rank()
numprocs = comm.Get_size()
memoryPool = None
if myid == masterNode:
# 若numpros不能整除N则需加上padding个无穷大的元素使能够整除
padding = numprocs - N % numprocs
memoryPool = np.empty(N + padding)
for i in range(0, N):
memoryPool[i] = (random.random()) * 100
if numprocs == 1:
# 单进程程序入口
time_start = time.time() # 串行开始计时
odd_even_sort(memoryPool, N)
time_end = time.time() # 串行停止计时
print('单个进程对 ', N, "个随机数进行冒泡排序,总时间:", time_end - time_start, "秒")
# 数据少的时候可以输出看结果
# for i in range(0, N - 1):
# print(memoryPool[i])
exit(0)
# 单进程程序出口
for i in range(0, padding):
memoryPool[N + i] = INF
part = int((N + padding) / numprocs)
# 并行开始计时
time_start = time.time()
part = comm.bcast(part if myid == masterNode else None, root=masterNode) # 广播part
data = np.empty(part)
comm.Scatter(memoryPool, data, root=masterNode) # 主进程向各进程分发数据
odd_even_sort(data, part) # 局部排序
print(myid, '号进程已完成对', part, '个元素的局部排序')
parallelMergeSort(data) # 奇偶融合
memoryPool = comm.gather(data, root=masterNode) # 将全局有序的数据收集到主进程
time_end = time.time()
# 并行停止计时
if myid == masterNode:
# 数据少的时候可以输出看结果
# for i in range(0, N - 1):
# print(memoryPool[i])
time.sleep(1)
print('调用', numprocs, '个进程对', N, "个随机数进行奇偶融合排序,总时间:", time_end - time_start, "秒")
运行结果
直接用pycharm运行程序,相当于进程数设置为1
在命令行中使用mpich2运行(cd 到个人的py文件所在目录,最后一个参数修改为py文件名)
增大问题规模和进程数量
小Tips:线程数量受限与设备的内存大小,我的笔记本电脑是8g+8g的内存条,最高可以设置进程数量为80,超过会报错显示内存不足。
本文参考了: