分数 评卷人 研究生课程报告 并行程序设计课程调研报告 学 号 M202373849 姓 名 朱旭鹏 指导教师 袁平鹏 专 业 计算机科学与技术 院 系 计算机科学与技术学院 2023 年 9 月 14 日 华 中 科 1 技 大 学 研 究 生 课 程 报 告 串行环境下快速排序复杂度分析 快速排序通过 partition 操作将长度将长度为 n 的数组分成 3 部分,假设左边 有 i 个元素,右边有 n-1-i 个元素,中间的基准值为 pivot。假设所有可能的输入 等概率出现,那么所有的划分情况也会等概率出现,即 i 会以相等的概率取区间 [0, n-1] 中的每一个值,所以快速排序的平均时间复杂度为 n − 1} | {z A[n] = + cost of partition n−1 X 1 [A(i) + A(n − 1 − i)], n ≥ 2 n |i=0 {z } cost of lef t and right parts 递归的初始情况:A[1]=A[0]=0,另外可以注意到左右两部分递归具有对称 性,所以有 n−1 X A(i) = i=0 n−1 X A(n − 1 − i) i=0 于是上述公式可以简化成如下形式: 2X A(n) = (n − 1) + A(i) n i=0 n 于是到了熟悉的数列求通项公式环节,以下为数学推导,采用错位相减法 nA(n) − (n − 1)A(n − 1) = n(n − 1) + 2 n−1 X A(i) i=0 =⇒ nA(n) = (n + 1)A(n − 1) + 2(n − 1) =⇒ A(n) A(n − 1) 2(n − 1) = + n+1 n n(n + 1) 考虑初始情况 B(1)=B(0)=0,接下来求解这个数列通项 B(n) = B(n − 1) + =⇒ B(n) − 2 4 − n+1 n 2 2 2 = B(n − 1) − + n+1 n n+1 1 华 中 科 技 大 学 研 究 生 课 程 报 告 2 2 ,于是有 C(n) = C(n − 1) + ,初始情况 n+1 n+1 P 2 为 C(1) = −1, C(0) = −2,所以 C(n) = ni=0 = O(logn),所以 B(n) = i+1 O(logn),A(n) = (n + 1)B(n) = O(nlogn),从而我们得知了快速排序的时间复杂 令 C(n) = B(n) − 度为 O(nlogn) 程序中仅开辟了一个一维数组对数据进行保存,易知快速排序程序的空间 复杂度为 O(n)。 2 简单多线程环境下的并行排序算法设计 编写使用多线程排序算法的 C 语言小程序,并按要求输出对应的排序后的 结果。 2.1 算法描述 使用多线程并行执行快速排序的基本算法流程见算法1。我们首先需要做一 些基础的前期工作, 首先先调用之前我们在串行排序部分写好的快速排序算法. 接着对快速排序的函数套上一层适用于 pthread 框架的接口, 因为 pthread 执行函 数只能传递一个指针, 我们只能把函数的参数合并到一个结构体里面,传递这个 结构体的指针。再在函数中解析这个结构体调用原生的快速排序接口, 接着设置 一个 barrier 要求每个进程都执行完快速排序才可以执行下面的操作。在下面的 叙述中, 我们假设 P 函数就是我们构造好的快速排序接口. 需要注意的是,本实验中需使用 pthread_barrier 函数保证所有线程都在分段 排好序之后,进行类似于归并排序的操作。由于每个段的长度可能会不一样,因 此需要定义两个数组分别保存每个段在数组中的起始位置和结束位置。由于在 创建线程时需要将串行排序函数以及串行排序函数的参数地址作为创建线程函 数的参数,基本的快速排序函数需要接收多个参数,因此将这些参数使用结构 体进行封装,在准备参数的时候,将其放到创建好的参数结构体中,把结构体指 针作为创建线程函数的参数传入即可。然而基本的串行快速排序函数没办法解 2 华 中 科 技 大 学 研 究 生 课 程 报 告 算法 1 多线程快速排序算法流程 Input: 数组的个数 n 以及 n 个数组元素, 线程数 N Output: 升序排序之后的数组 1: 接收用户输入,进行数组的初始化; 初始化一个 pthread_barrier_t 变量, 以便 2: 后续进行线程的同步操作;进行数组的分段操作。 N 计算分段的大小为 k = ,其中 N 是任务的总数,T 是可以存在的线程数。 T 将每个分段的起止索引进行保存, 第 i 个分段 (0 ≤ i < T ) 的起止索引为 3: [ik, (i + 1) ∗ k − 1]。 4: 为每个分段创建一个线程,对每个分段数组进行快速排序 (P 函数) 操作。等 待所有分段排序完成之后进行下一步。 5: 对每一个已经排好序的段进行合并。合并策略是:为每个分段设置一个指针, 初始时都指向每个分段的第一个元素,然后每次选取这些指针所指元素中最 小的一个放至到结果集,被选择的指针需要向后移动,然后继续进行比较操 作,直到所有的段合并完成。 析以结构体方式传入的参数,因此需要把对基本的快速排序函数的调用封装到 pthread_sort 函数中,在这个函数里面首先解析参数结构体里面的字段,然后将 特定的参数送至基本的快速排序函数,对其进行调用。 2.2 复杂度分析 在这里我们的分析假设 T 已知. 我们分成并行的 Span 以及串行的 Work. 首 先 我 们 来 分 析 串 行 的 复 杂 度, 对 于 快 速 排 序, 我 们 可 以 证 明 它 的 复 杂 度 是 n n O(n log n), 那么对于排序, 我们首先要执行 T 个小排序, 复杂度是 T · log , 最 T T 后需要花费 nT 的时间把所有的结果 merge 在一起. 所以说总体的时间复杂度是 O(n log n), 在串行的情况下没有太大的提升. 接着我们来分析并行的效率, 对于 n 排序, 这 T 个小排序是同时执行的, 所以说, 复杂度是 df racnT log . 最后需要花 T 费 nT 的 时 间 把 所 有 的 结 果 merge 在 一 起. 所 以 说 总 体 的 时 间 复 杂 度 是 n O log n . 这 个 排 序 的 优 越 性 取 决 于 T 取 值 的 权 衡. 总 体, 串 行 的 T W ork = O(n log n). 并行的 Span = O(n log n). 但是并行的常数更好. 对于空间复杂度, 我使用了一维数组, 没有开新的数组存放数据, 空间复杂度 3 华 中 科 技 大 学 研 究 生 课 程 报 告 为 O(n). 2.3 实验结果和分析 假设使用线程的数目为 3,输入数据:8 49 38 65 97 76 13 27 49 排序过程过程如下: 首先进行分段,分段数目为 3: {49, 38}, {65, 97}, {76, 13, 27, 49} 对每个分段进行快速排序之后: {38, 49}, {65, 97}, {13, 27, 49, 76} 对每个分段进行合并之后: {13, 27, 38, 49, 49, 65, 76, 97} 最后得到升序序列为 {13, 27, 38, 49, 49, 65, 76, 97} 3 3.1 OpenMP 环境下的并行排序算法设计 算法描述 OpenMP 是一种用于共享内存并行系统的多线程程序设计方案,OpenMP 提 供了对并行算法的高层抽象描述,提供多种编译制导指令,这些指令以 #pragma omp 开头,后边跟具体的功能指令,格式为:#pragma omp 指令 [子句][, 子句]...。 我们在编程时只需要在顶层设计中给出并行化执行策略,而无需考虑底层的实 现。支持 OpenMP 的编译环境能够根据程序中添加的 pragma 指令,自动将程序 进行并行的处理。 在本实验中我使用了如下几个编译制导指令: (1) #pragma omp parallel:该编译制导指令用一个结构块之前,表示这段 代码块将被多个线程并行执行。 (2) #pragma omp task firstprivate(list):该编译制导指令中的 task 指令表示定义一个显示的任务,线程组内的某一个线程会来执行此任务。task 4 华 中 科 技 大 学 研 究 生 课 程 报 告 的出现很好的解决了 for 和 sections 指令的“缺陷”:无法根据运行时的环 境动态进行任务划分的,必须预先知道任务的划分情况。task 指令是动态定义 任务的,在运行过程中,使用一个 task 就会定义一个任务,任务就会在一个 线程上面去执行,task 还能够进行任务的嵌套定义,适用于递归的情况,对于 快速排序非常适用。子句 firstprivate 用于指定 list 中的变量在每个线程 中都会有一份私有副本,且私有变量要在进入并行域或者任务分担域时,继承 主线程中的同名变量作为初始值。使得 list 中的私有数据不受外部同名变量 的影响。 (3) #pragma omp single nowait:single 选项是在并行块中使用的,它告 诉编译器,接下来紧跟的代码将只会有一个线程执行;可能在处理多线程不安全 代码时非常有用;nowait 子句指出并发线程可以忽略其他制导指令暗含的路障 同步。 算法3中,仅对 OpenMP 的编译制导指令使用在哪些地方进行描述。其他部 分不再赘述。 算法 2 基于 OpenMP 的快速排序算法 Input: 数组的个数 n 以及 n 个数组元素, 线程数 N Output: 升序排序之后的数组 1: 开设 8 个线程来执行并行代码段,将主程序中调用快速排序的语句块用花括 号 {} 括起来,在花括号上面指定 #pragma omp parallel,表明并行执行快速 排序程序 2: 在并行块中,调用快速排序函数的语句前指定 #pragma omp single nowait 表 示主程序中对快速排序的总调用只能由第 0 号线程执行一次,其他线程不能 执行。 3: 在 快 速 排 序 程 序 的 递 归 调 用 语 句 之 前 指 定 #pragma omp task firstprivate(arr,start,keyIndex) 表 示 为 递 归 子 程 序 的 调 用 定 义 任 务, 且 为 任务创建私有副本,其中 arr 是数组,start 是开始的索引,keyIndex 是基准 值的索引 5 华 中 科 技 大 学 研 究 生 课 程 报 告 复杂度分析 3.2 下面仅根据我个人对于 OpenMP 程序的理解给出复杂度的分析过程,由于 我不清楚对编译器会具体怎么对线程进行调度,所以我的理解不一定正确。 在算法描述部分已经给出的对 OpenMP 的编译制导指令的使用,可以知道 最重要的一部分在快速排序的递归调用的任务定义上面。由于每一次递归调用 都会定义一个任务,我们可以把这个递归调用想象成一棵树,如图1所示。 图1 快速排序递归过程 在这里我们的分析假设 T (OpenMP 将其化成 N 个进程) 已知. 我们分成并行 的 Span 以及串行的 Work. 首先我们来分析串行的复杂度, 对于快速排序, 我们可 以证明它的复杂度是 O(n log n), 那么对于排序, 我们首先要执行 T 个小排序, 复 n n 杂度是 T · log , 最后需要花费一定的时间把所有的结果 merge 在一起 (因为 T T 我们并不知道 OpenMP 怎样 merge). 所以说总体的时间复杂度是 O(n log n), 在 串行的情况下没有太大的提升. 接着我们来分析并行的效率, 对于排序, 这 T 个 n 小排序是同时执行的, 所以说, 复杂度是 dfracnTlog . 最后需要花费一定的时间 T n 把所有的结果 merge 在一起. 所以说总体的时间复杂度是 O log n . 这个排序 T 的优越性取决于 T 取值的权衡. 总体, 串行的 W ork = O(n log n). 并行的 Span = O(n log n). 但是并行的常数更好. 如果 OpenMP 能在每一次快速排序的时候生 成多个 task 执行, 这些 task 能够完全并行化之行, 那么排序的 Span 会被优化到 O(n). Span(n) = Merge(n) + Span Span(n) = O(n) 6 n 2 华 中 科 技 大 学 研 究 生 课 程 报 告 对于空间复杂度, 我使用了一维数组, 没有开新的数组存放数据, 空间复杂度为 O(n). 3.3 实验结果和分析 仍然使用之前的输入: 8 49 38 65 97 76 13 27 49 但由于任务执行或者说线程的调度的不确定性,其中间执行过程不一定每 一次都是一样的,但是不论中间过程是怎样的,总体执行的流程还是与图1所示 的递归树一致。执行的结果是 {13, 27, 38, 49, 49, 65, 76, 97} 4 4.1 MPI 环境下的并行排序算法设计 算法描述 MPI 是一个跨语言(编程语言如 C, Fortran 等)的通讯协议,用于编写并行 计算机。支持点对点和广播。MPI 是一个信息传递应用程序接口,包括协议和和 语义说明,他们指明其如何在各种实现中发挥其特性。MPI 的目标是高性能,大 规模性,和可移植性。MPI 在今天仍为高性能计算的主要模型。与 OpenMP 并行 程序不同,MPI 是一种基于信息传递的并行编程技术。消息传递接口是一种编 程接口标准,而不是一种具体的编程语言。简而言之,MPI 标准定义了一组具有 可移植性的编程接口。 假设我们已经知道 MPI 的实现方式比较朴素的思想就是, 我们把数组划成 T 部分, 对每一个部分分别做一次归并排序, 然后对这些排好序的数组进行合并操 作. 下面我们假设 T = 2, 解析一下算法的实现过程: 1 号进程收到了 0 号进程的数据,进行排序,将排序的结果发给 0 号进程。 4.2 复杂度分析 使用 MPI 的消息通信机制的并行程序,其时间复杂度的大小与数组元素的 个数和进程的数目有关,进程数目也不是越多越好,因为这样会加剧通信的开销 7 华 中 科 技 大 学 研 究 生 课 程 报 告 算法 3 基于 MPI 的归并排序算法–0 号进程 Input: 数组 A 和数组的大小 n Output: 升序排序之后的数组 1: 进行相关的初始化, 获取数组的大小、数组的内容. 假设数组的大小为 n, 把 n 发送给 1 号进程 2: 3: n 获取到的数组内容, 把 [ , 1] 这一个部分通过消息发送给 1 号进程。 2 n 0 号进程对 [0−, − 1] 的进程进行排序。收到了 1 号进程的排序结果就进行 2 归并 以降低程序执行的效率。但不管怎样,该并行程序的时间复杂度一定 ≲ O(nlogn)。 程序中仅开辟了一个一维数组对数据进行保存,空间复杂度为 O(n)。 8