末并的算法
① 并行处理的并行算法的基本策略
在并行处理技术中所使用的算法主要遵循三种策略:
1.分而治之法:也就是把多个任务分解到多个处理器或多个计算机中,然后再按照一定的拓扑结构来进行求解。
2.重新排序法:分别采用静态或动态的指令词度方式。
3.显式/隐式并行性结合:显式指的是并行语言通过编译形成并行程序,隐式指的是串行语言通过编译形成并行程序,显式/隐式并行性结合的关键就在于并行编译,而并行编译涉及到语句、程序段、进程以及各级程序的并行性。
二、并行性描述定义
利用计算机语言进行并行性描述的时候主要有三种方案:
1.语言扩展方案:也就是利用各种语言的库函数来进行并行性功能的扩展。
2.编译制导法:也称为智能编译,它是隐式并行策略的体现,主要是由并行编译系统进行程序表示、控制流的分析、相关分析、优化分析和并行化划分,由相关分析得到方法库管理方案,由优化分析得到知识库管理方案,由并行化划分得到程序重构,从而形成并行程序。
3.新的语言结构法:这是显式并行策略的体现。也就是建立一种全新的并行语言的体系,而这种并行语言通过编译就能直接形成并行程序。
三、并行软件
并行软件可分成并行系统软件和并行应用软件两大类,并行系统软件主要指并行编译系统和并行操作系统,并行应用软件主要指各种软件工具和应用软件包。在软件中所牵涉到的程序的并行性主要是指程序的相关性和网络互连两方面。
1.程序的相关性:程序的相关性主要分为数据相关、控制相关和资源相关三类。
数据相关说明的是语句之间的有序关系,主要有流相关、反相关、输出相关、I/O相关和求知相关等,这种关系在程序运行前就可以通过分析程序确定下来。数据相关是一种偏序关系,程序中并不是每一对语句的成员都是相关联的。可以通过分析程序的数据相关,把程序中一些不存在相关性的指令并行地执行,以提高程序运行的速度。
控制相关指的是语句执行次序在运行前不能确定的情况。它一般是由转移指令引起的,只有在程序执行到一定的语句时才能判断出语句的相关性。控制相关常使正在开发的并行性中止,为了开发更多的并行性,必须用编译技术克服控制相关。
而资源相关则与系统进行的工作无关,而与并行事件利用整数部件、浮点部件、寄存器和存储区等共享资源时发生的冲突有关。软件的并行性主要是由程序的控制相关和数据相关性决定的。在并行性开发时往往把程序划分成许多的程序段——颗粒。颗粒的规模也称为粒度,它是衡量软件进程所含计算量的尺度,一般用细、中、粗来描述。划分的粒度越细,各子系统间的通信时延也越低,并行性就越高,但系统开销也越大。因此,我们在进行程序组合优化的时候应该选择适当的粒度,并且把通讯时延尽可能放在程序段中进行,还可以通过软硬件适配和编译优化的手段来提高程序的并行度。
2.网络互连:将计算机子系统互连在一起或构造多处理机或多计算机时可使用静态或动态拓扑结构的网络。静态网络由点一点直接相连而成,这种连接方式在程序执行过程中不会改变,常用来实现集中式系统的子系统之间或分布式系统的多个计算结点之间的固定连接。动态网络是用开关通道实现的,它可动态地改变结构,使之与用户程序中的通信要求匹配。动态网络包括总线、交叉开关和多级网络,常用于共享存储型多处理机中。在网络上的消息传递主要通过寻径来实现。常见的寻径方式有存储转发寻径和虫蚀寻径等。在存储转发网络中以长度固定的包作为信息流的基本单位,每个结点有一个包缓冲区,包从源结点经过一系列中间结点到达目的结点。存储转发网络的时延与源和目的之间的距离(段数)成正比。而在新型的计算机系统中采用虫蚀寻径,把包进一步分成一些固定长度的片,与结点相连的硬件寻径器中有片缓冲区。消息从源传送到目的结点要经过一系列寻径器。同一个包中所有的片以流水方式顺序传送,不同的包可交替地传送,但不同包的片不能交叉,以免被送到错误的目的地。虫蚀寻径的时延几乎与源和目的之间的距离无关。在寻径中产生的死锁问题可以由虚拟通道来解决。虚拟通道是两个结点间的逻辑链,它由源结点的片缓冲区、结点间的物理通道以及接收结点的片缓冲区组成。物理通道由所有的虚拟通道分时地共享。虚拟通道虽然可以避免死锁,但可能会使每个请求可用的有效通道频宽降低。因此,在确定虚拟通道数目时,需要对网络吞吐量和通信时延折衷考虑。
四、硬件技术在硬件技术方面主要从处理机、存储器和流水线三个方面来实现并行。
1.处理机:主要的处理机系列包括CISC、RISC、超标量、VL1W、超流水线、向量以及符号处理机。
传统的处理机属于复杂指令系统计算(CISC)结构。指令系统大,指令格式可变,通用寄存器个数较少,基本上使用合一的指令与数据高速缓存,时钟频率较低,CPI较高,大多数利用ROM 实现微码控制CPU,而当今的精简指令系统计算(RISC)处理机指令格式简单规范,面向寄存器堆,采用重叠寄存器窗口技术,具有多级Cache,多种流水线结构,强调编译优化技术,时钟频率快,CPI低,大多数用硬连线控制CPU。
CISC或RISC标量处理机都可以采用超标量或向量结构来改善性能。标量处理机在每个周期内只发射一条指令并要求周期只完成从流水线来的一条指令。而在超标量处理机中,使用了多指令流水线,每个周期要发射多条指令并产生多个结果。由于希望程序中有许多的指令级并行性,因此超标量处理机更要依靠优化编译器去开发并行性。
VL1W 结构是将水平微码和超标量处理这两种普遍采用的概念结合起来产生的。典型的超长指令字VL1W 机器指令字长度有数百位。在VLlW 处理机中,多个功能部件是并发工作的,所有的功能部件共享使用公用大型寄存器堆,由功能部件同时执行的各种操作是用VL1W 指令来同步的,每条指令可指定多个操作。VL1W 指令译码比超标量指令容易,但在开发不同数量的并行性时总是需要不同的指令系统。VL1W 主要是开发标量操作之间的并行性,它的成功与否很大程度取决于代码压缩的效率,其结构和任何传统的通用处理机完全不兼容。即使同一结构的不同实现也不大可能做到彼此二进制兼容。VL1W 的主要优点在于它的硬件结构和指令系统简单,在科学应用领域可以发挥良好作用,但在一般应用场合可能并不很好用。
向量处理机对数组执行向量指令,每条指令都包含一串重复的操作。它是专门设计用来完成向量运算的协处理机,通常用于多流水线超级计算机中。向量处理机可以利用循环级展开所得的并行性,它可以附属于任何标量处理机。专用的向量流水线可以在循环控制中消除某些软件开销,它的效果与优化编译器将顺序代码向量化的性能很有关系。从理论上说,向量机可以具有和超标量处理机同样的性能,因此可以说向量机的并行性与超标量机相同。
符号处理机是为AI应用而研制的,已用于定理证明、模式识别、专家系统、知识工程、文本检索、科学以及机器智能等许多应用领域。在这些应用中,数据和知识表达式、原语操作、算法特性、存储器、I/0和通信以及专用的结构特性与数值计算是不一样的,符号处理机也称为逻辑程序设计语言处理机、表处理语言处理机或符号变换器。符号处理并不和数值数据打交道,它处理的是逻辑程序、符号表、对象、剧本、黑板、产生式系统、语义网络、框架以及人工神经网络等问题。这些操作需要专门的指令系统,通常不使用浮点操作。
2.存储器:存储设备按容量和存取时间从低到高可分为寄存器、高速缓存、主存储器、磁盘设备和磁带机五个层次。较低层存储设备与较高层的相比,存取速度较快、容量较小,每字节成本较高、带宽较宽、传输单位较小。
存放在存储器层次结构中的信息满足三个重要特性:包含性、一致性和局部性。所谓包含性,指的是一个信息字的复制品可以在比它高的所有层中找到,而如果在高层中丢失了一个信息,则在比它低的所有层中此信息也将丢失。CPU 和高速缓存之间的信息传送是按字进行的,高速缓存和主存储器间用块作为数据传送的基本单位,主存和磁盘之间又是以页面为基本单位来传送信息的,而在磁盘和磁带机之间的数据传送则是按文件级处理的。所谓一致性要求的是同一个信息项与后继存储器层次上的副本是一致的。也就是说,如果在高速缓存中的一个字被修改过,那么在所有更高层上该字的副本也必须立即或最后加以修改。为了尽量减少存储器层次结构的有效存取时间,通常把频繁使用的信息放在较低层次。维护存储器层次结构一致性一般有两种策略,一种是写直达策略,也就是如果,则立即在所有高层存储器中进行同样的修改;另一种是写回策略,也就是在较低层中对信息进行修改后并不立即在高层存储器中进行相应的修改,而是等到该信息将被替换或将从低层中消失时才在所有高层存储器中进行同样的修改。甚至可以将写直达和写回策略的优点结合起来,形成写一次协议来维护存储器的一致性。
存储器的层次结构是在一种程序行为——访问的局部性基础上开发出来的。主要有时间局部性、空间局部性和顺序局部性。时间局部性指的是最近的访问项很可能在不久的将来再次被访问。它往往会引起对最近使用区域的集中访问。空间局部性表示一种趋势,指的是一个进程访问的各项其地址彼此很近。顺序局部性指的是在典型程序中,除非是转移指令,一般指令都是顺序执行的。
在多处理机系统中一般使用共享存储器。对共享存储器的组织一般采用低位交叉、高位交叉、高低位交叉三种方法。低位交叉又称并发存取,它是把相邻的地址放在相邻的存储器模块中,在访问时不容易产生冲突,并行性较好,但可靠性容错能力和扩展性均较差。高位交叉又称允许同时存取,它是把相邻地址分配到同一个存储器模块中,可靠性、容错能力和扩展性均较强,但访问时易产生冲突,带宽较窄,并行性较差。高低位交叉存取又称C—s存取,它是结合了高位交叉和低位交叉两种方法的优点,既解决了冲突问题,又能有效地提高容错能力和并行性,最适合于向量处理机结构。
3.流水线:流水线技术主要有指令流水线技术和运算流水线技术两种。
指令流水线技术主要目的是要提高计算机的运行效率和吞吐率。它主要通过设置预取指令缓冲区、设置多功能部件、进行内部数据定向、采取适当的指令调度策略来实现。指令调度的策略主要有静态和动态两种,静态词度是基于软件的,主要由编译器完成,动态词度是基于硬件的,主要是通过硬件技术进行。
运算流水线主要有单功能流水线和多功能流水线两种。其中多功能流水线又可分为静态流水线和动态流水线。静态流水线技术只用来实现确定的功能,而动态流水线可以在不同时间重新组合,实现不同的功能,它除流线连接外,还允许前馈和反馈连接,因此也称为非线性流水线。这些前馈和反馈连接使得进入流水线的相继事件的词度变得很不简单。由于这些连接,流水线不一定从最后一段输出。根据不同的数据流动模式,人们可以用同一条流水线求得不同功能的值。
并行计算机发展简述
40 年代开始的现代计算机发展历程可以分为两个明显的发展时代:串行计算时代、并行计算时代。每一个计算时代都从体系结构发展开始,接着是系统软件(特别是编译器与操作系统)、应用软件,最后随着问题求解环境的发展而达到顶峰。创建和使用并行计算机的主要原因是因为并行计算机是解决单处理器速度瓶颈的最好方法之一。
并行计算机是由一组处理单元组成的,这组处理单元通过相互之间的通信与协作,以更快的速度共同完成一项大规模的计算任务。因此,并行计算机的两个最主要的组成部分是计算节点和节点间的通信与协作机制。并行计算机体系结构的发展也主要体现在计算节点性能的提高以及节点间通信技术的改进两方面。
60 年代初期,由于晶体管以及磁芯存储器的出现,处理单元变得越来越小,存储器也更加小巧和廉价。这些技术发展的结果导致了并行计算机的出现,这一时期的并行计算机多是规模不大的共享存储多处理器系统,即所谓大型主机(Mainframe)。IBM360 是这一时期的典型代表。
到了60 年代末期,同一个处理器开始设置多个功能相同的功能单元,流水线技术也出现了。与单纯提高时钟频率相比,这些并行特性在处理器内部的应用大大提高了并行计算机系统的性能。伊利诺依大学和Burroughs 公司此时开始实施IlliacIV 计划,研制一台64 个CPU 的SIMD 主机系统,它涉及到硬件技术、体系结构、I/O 设备、操作系统、程序设计语言直至应用程序在内的众多研究课题。不过,当一台规模大大缩小了的16CPU 系统终于在1975 年面世时,整个计算机界已经发生了巨大变化。
首先是存储系统概念的革新,提出虚拟存储和缓存的思想。IBM360/85 系统与360/91是属于同一系列的两个机型,360/91 的主频高于360/85,所选用的内存速度也较快,并且采用了动态调度的指令流水线;但是,360/85 的整体性能却高于360/91,唯一的原因就是前者采用了缓存技术,而后者则没有。
其次是半导体存储器开始代替磁芯存储器。最初,半导体存储器只是在某些机器被用作缓存,而CDC7600 则率先全面采用这种体积更小、速度更快、可以直接寻址的半导体存储器,磁芯存储器从此退出了历史舞台。与此同时,集成电路也出现了,并迅速应用到了计算机中。元器件技术的这两大革命性突破,使得IlliacIV 的设计者们在底层硬件以及并行体系结构方面提出的种种改进都大为逊色。
1976 年CRAY-1 问世以后,向量计算机从此牢牢地控制着整个高性能计算机市场15 年。CRAY-1 对所使用的逻辑电路进行了精心的设计,采用了我们如今称为RISC 的精简指令集,还引入了向量寄存器,以完成向量运算。这一系列全新技术手段的使用,使CRAY-1 的主频达到了80MHz。
微处理器随着机器的字长从4 位、8 位、16 位一直增加到32 位,其性能也随之显着提高。正是因为看到了微处理器的这种潜力,卡内基- 梅隆大学开始在当时流行的DECPDP11 小型计算机的基础上研制成功一台由16 个PDP11/40 处理机通过交叉开关与16 个共享存储器模块相连接而成的共享存储多处理器系统C.mmp。
从80 年代开始,微处理器技术一直在高速前进。稍后又出现了非常适合于SMP 方式的总线协议,而伯克利加州大学则对总线协议进行了扩展,提出了Cache 一致性问题的处理方案。从此,C.mmp 开创出的共享存储多处理器之路越走越宽;现在,这种体系结构已经基本上统治了服务器和桌面工作站市场。
同一时期,基于消息传递机制的并行计算机也开始不断涌现。80 年代中期,加州理工成功地将64 个i8086/i8087 处理器通过超立方体互连结构连结起来。此后,便先后出现了Intel iPSC 系列、INMOS Transputer 系列,Intel Paragon 以及IBM SP 的前身Vulcan 等基于消息传递机制的并行计算机。
80 年代末到90 年代初,共享存储器方式的大规模并行计算机又获得了新的发展。IBM将大量早期RISC 微处理器通过蝶形互连网络连结起来。人们开始考虑如何才能在实现共享存储器缓存一致的同时,使系统具有一定的可扩展性(Scalability)。90 年代初期,斯坦福大学提出了DASH 计划,它通过维护一个保存有每一缓存块位置信息的目录结构来实现分布式共享存储器的缓存一致性。后来,IEEE 在此基础上提出了缓存一致性协议的标准。
90 年代以来,主要的几种体系结构开始走向融合。属于数据并行类型的CM-5 除大量采用商品化的微处理器以外,也允许用户层的程序传递一些简单的消息;CRAY T3D是一台NUMA 结构的共享存储型并行计算机,但是它也提供了全局同步机制、消息队列机制,并采取了一些减少消息传递延迟的技术。
随着商品化微处理器、网络设备的发展,以及MPI/PVM 等并行编程标准的发布,机群架构的并行计算机出现。IBM SP2 系列机群系统就是其中的典型代表。在这些系统中,各个节点采用的都是标准的商品化计算机,它们之间通过高速网络连接起来。
今天,越来越多的并行计算机系统采用商品化的微处理器加上商品化的互连网络构造,这种分布存储的并行计算机系统称为机群。国内几乎所有的高性能计算机厂商都生产这种具有极高性能价格比的高性能计算机,并行计算机就进入了一个新的时代,并行计算的应用达到了前所未有的广度和深度。
并行计算机随着微处理芯片的发展,已经进入了一个新时代。目前并行计算机的性能已经突破20PFLOPS,正在向百亿亿次发展。我国并行计算机的研制已经走在世界前列。2003年由联想公司生产的深腾6800 在2003 年11 月世界TOP500 排名中位列第14 名,2004 年曙光公司生产的曙光4000A 在2004 年6 月的世界TOP500 排名中位列第10 名,这是我国公开发布的高性能计算机在世界TOP500 中首次进入前十名,这标志着我国在并行计算机系统的研制和生产中已经赶上了国际先进水平,为提高我国的科学研究水平奠定了物质基础。2013年国际超级计算机大会最新发布的世界超级计算机500强排名中,国防科技大学研制的天河二号超级计算机系统,以峰值计算速度每秒5.49亿亿次、持续计算速度每秒3.39亿亿次双精度浮点运算的优异性能位居榜首。
从TOP500 的前10 名来看,美国仍然是超级计算机的最大拥有者。按照世界TOP500 的统计数据来分析,美国在计算能力上占有近全世界的一半,在TOP500 中的所有计算机中拥有的数量超过50%。
② 归并排序
先考虑一个简单的问题:如何在线性的时间内将两个有序队列合并为一个有序队列(并输出)?
A队列:1 3 5 7 9
B队列:1 2 7 8 9
看上面的例子,AB两个序列都是已经有序的了。在给出数据已经有序的情况下,我们会发现很多神奇的事,比如,我们将要输出的第一个数一定来自于这两个序列各自最前面的那个数。两个数都是1,那么我们随便取出一个(比如A队列的那个1)并输出:
A队列:1 3 5 7 9
B队列:1 2 7 8 9
输出:1
注意,我们取出了一个数,在原数列中删除这个数。删除操作是通过移动队首指针实现的,否则复杂度就高了。
现在,A队列打头的数变成3了,B队列的队首仍然是1。此时,我们再比较3和1哪个大并输出小的那个数:
A队列:1 3 5 7 9
B队列:1 2 7 8 9
输出:1 1
接下来的几步如下:
A队列:1 3 5 7 9 A队列:1 3 5 7 9 A队列:1 3 5 7 9 A队列:1 3 5 7 9
B队列:1 2 7 8 9 ==> B队列:1 2 7 8 9 ==> B队列:1 2 7 8 9 ==> B队列:1 2 7 8 9 ……
输出:1 1 2 输出:1 1 2 3 输出:1 1 2 3 5 输出:1 1 2 3 5 7
我希望你明白了这是怎么做的。这个做法显然是正确的,复杂度显然是线性。
归并排序(Merge Sort)将会用到上面所说的合并操作。给出一个数列,归并排序利用合并操作在O(nlogn)的时间内将数列从小到大排序。归并排序用的是分治(Divide and Conquer)的思想。首先我们把给出的数列平分为左右两段,然后对两段数列分别进行排序,最后用刚才的合并算法把这两段(已经排过序的)数列合并为一个数列。有人会问“对左右两段数列分别排序时用的什么排序”么?答案是:用归并排序。也就是说,我们递归地把每一段数列又分成两段进行上述操作。你不需要关心实际上是怎么操作的,我们的程序代码将递归调用该过程直到数列不能再分(只有一个数)为止。
初看这个算法时有人会误以为时间复杂度相当高。我们下面给出的一个图将用非递归的眼光来看归并排序的实际操作过程,供大家参考。我们可以借助这个图证明,归并排序算法的时间复杂度为O(nlogn)。
[3] [1] [4] [1] [5] [9] [2] [7]
\ / \ / \ / \ /
[1 3] [1 4] [5 9] [2 7]
\ / \ /
[1 1 3 4] [2 5 7 9]
\ /
[1 1 2 3 4 5 7 9]
上图中的每一个“ \ / ”表示的是上文所述的线性时间合并操作。上图用了4行来图解归并排序。如果有n个数,表示成上图显然需要O(logn)行。每一行的合并操作复杂度总和都是O(n),那么logn行的总复杂度为O(nlogn)。这相当于用递归树的方法对归并排序的复杂度进行了分析。假设,归并排序的复杂度为T(n),T(n)由两个T(n/2)和一个关于n的线性时间组成,那么T(n)=2*T(n/2)+O(n)。不断展开这个式子我们可以同样可以得到T(n)=O(nlogn)的结论,你可以自己试试。如果你能在线性的时间里把分别计算出的两组不同数据的结果合并在一起,根据T(n)=2*T(n/2)+O(n)=O(nlogn),那么我们就可以构造O(nlogn)的分治算法。这个结论后面经常用。我们将在计算几何部分举一大堆类似的例子。
如果你第一次见到这么诡异的算法,你可能会对这个感兴趣。分治是递归的一种应用。这是我们第一次接触递归运算。下面说的快速排序也是用的递归的思想。递归程序的复杂度分析通常和上面一样,主定理(Master Theory)可以简化这个分析过程。主定理和本文内容离得太远,我们以后也不会用它,因此我们不介绍它,大家可以自己去查。有个名词在这里的话找学习资料将变得非常容易,我最怕的就是一个东西不知道叫什么名字,半天找不到资料。
归并排序有一个有趣的副产品。利用归并排序能够在O(nlogn)的时间里计算出给定序列里逆序对的个数。你可以用任何一种平衡二叉树来完成这个操作,但用归并排序统计逆序对更方便。我们讨论逆序对一般是说的一个排列中的逆序对,因此这里我们假设所有数不相同。假如我们想要数1, 6, 3, 2, 5, 4中有多少个逆序对,我们首先把这个数列分为左右两段。那么一个逆序对只可能有三种情况:两个数都在左边,两个数都在右边,一个在左一个在右。在左右两段分别处理完后,线性合并的过程中我们可以顺便算出所有第三种情况的逆序对有多少个。换句话说,我们能在线性的时间里统计出A队列的某个数比B队列的某个数大有多少种情况。
A队列:1 3 6 A队列:1 3 6 A队列:1 3 6 A队列:1 3 6 A队列:1 3 6
B队列:2 4 5 ==> B队列:2 4 5 ==> B队列:2 4 5 ==> B队列:2 4 5 ==> B队列:2 4 5 ……
输出: 输出:1 输出:1 2 输出:1 2 3 输出:1 2 3 4
每一次从B队列取出一个数时,我们就知道了在A队列中有多少个数比B队列的这个数大,它等于A队列现在还剩的数的个数。比如,当我们从B队列中取出2时,我们同时知道了A队列的3和6两个数比2大。在合并操作中我们不断更新A队列中还剩几个数,在每次从B队列中取出一个数时把当前A队列剩的数目加进最终答案里。这样我们算出了所有“大的数在前一半,小的数在后一半”的情况,其余情况下的逆序对在这之前已经被递归地算过了。
============================华丽的分割线============================
堆排序(Heap Sort)利用了堆(Heap)这种数据结构(什么是堆?)。堆的插入操作是平均常数的,而删除一个根节点需要花费O(log n)的时间。因此,完成堆排序需要线性时间建立堆(把所有元素依次插入一个堆),然后用总共O(nlogn)的时间不断取出最小的那个数。只要堆会搞,堆排序就会搞。堆在那篇日志里有详细的说明,因此这里不重复说了。
============================华丽的分割线============================
快速排序(Quick Sort)也应用了递归的思想。我们想要把给定序列分成两段,并对这两段分别进行排序。一种不错的想法是,选取一个数作为“关键字”,并把其它数分割为两部分,把所有小于关键字的数都放在关键字的左边,大于关键字的都放在右边,然后递归地对左边和右边进行排序。把该区间内的所有数依次与关键字比较,我们就可以在线性的时间里完成分割的操作。完成分割操作有很多有技巧性的实现方法,比如最常用的一种是定义两个指针,一个从前往后找找到比关键字大的,一个从后往前找到比关键字小的,然后两个指针对应的元素交换位置并继续移动指针重复刚才的过程。这只是大致的方法,具体的实现还有很多细节问题。快速排序是我们最常用的代码之一,网上的快速排序代码五花八门,各种语言,各种风格的都有。大家可以随便找一个来看看,我说过了我们讲算法但不讲如何实现。NOIp很简单,很多人NOIp前就背了一个快速排序代码就上战场了。当时我把快速排序背完了,抓紧时间还顺便背了一下历史,免得晚上听写又不及格。
不像归并排序,快速排序的时间复杂度很难计算。我们可以看到,归并排序的复杂度最坏情况下也是O(nlogn)的,而快速排序的最坏情况是O(n^2)的。如果每一次选的关键字都是当前区间里最大(或最小)的数,那么这样将使得每一次的规模只减小一个数,这和插入排序、选择排序等平方级排序没有区别。这种情况不是不可能发生。如果你每次选择关键字都是选择的该区间的第一个数,而给你的数据恰好又是已经有序的,那你的快速排序就完蛋了。显然,最好情况是每一次选的数正好就是中位数,这将把该区间平分为两段,复杂度和前面讨论的归并排序一模一样。根据这一点,快速排序有一些常用的优化。比如,我们经常从数列中随机取一个数当作是关键字(而不是每次总是取固定位置上的数),从而尽可能避免某些特殊的数据所导致的低效。更好的做法是随机取三个数并选择这三个数的中位数作为关键字。而对三个数的随机取值反而将花费更多的时间,因此我们的这三个数可以分别取数列的头一个数、末一个数和正中间那个数。另外,当递归到了一定深度发现当前区间里的数只有几个或十几个时,继续递归下去反而费时,不如返回插入排序后的结果。这种方法同时避免了当数字太少时递归操作出错的可能。
下面我们证明,快速排序算法的平均复杂度为O(nlogn)。不同的书上有不同的解释方法,这里我选用算法导论上的讲法。它更有技巧性一些,更有趣一些,需要转几个弯才能想明白。
看一看快速排序的代码。正如我们提到过的那种分割方法,程序在经过若干次与关键字的比较后才进行一次交换,因此比较的次数比交换次数更多。我们通过证明一次快速排序中元素之间的比较次数平均为O(nlogn)来说明快速排序算法的平均复杂度。证明的关键在于,我们需要算出某两个元素在整个算法过程中进行过比较的概率。
我们举一个例子。假如给出了1到10这10个数,第一次选择关键字7将它们分成了{1,2,3,4,5,6}和{8,9,10}两部分,递归左边时我们选择了3作为关键字,使得左部分又被分割为{1,2}和{4,5,6}。我们看到,数字7与其它所有数都比较过一次,这样才能实现分割操作。同样地,1到6这6个数都需要与3进行一次比较(除了它本身之外)。然而,3和9决不可能相互比较过,2和6也不可能进行过比较,因为第一次出现在3和9,2和6之间的关键字把它们分割开了。也就是说,两个数A(i)和A(j)比较过,当且仅当第一个满足A(i)<=x<=A(j)的关键字x恰好就是A(i)或A(j) (假设A(i)比A(j)小)。我们称排序后第i小的数为Z(i),假设i<j,那么第一次出现在Z(i)和Z(j)之间的关键字恰好就是Z(i)或Z(j)的概率为2/(j-i+1),这是因为当Z(i)和Z(j)之间还不曾有过关键字时,Z(i)和Z(j)处于同一个待分割的区间,不管这个区间有多大,不管递归到哪里了,关键字的选择总是随机的。我们得到,Z(i)和Z(j)在一次快速排序中曾经比较过的概率为2/(j-i+1)。
现在有四个数,2,3,5,7。排序时,相邻的两个数肯定都被比较过,2和5、3和7都有2/3的概率被比较过,2和7之间被比较过有2/4的可能。也就是说,如果对这四个数做12次快速排序,那么2和3、3和5、5和7之间一共比较了12*3=36次,2和5、3和7之间总共比较了8*2=16次,2和7之间平均比较了6次。那么,12次排序中总的比较次数期望值为36+16+6=58。我们可以计算出单次的快速排序平均比较了多少次:58/12=29/6。其实,它就等于6项概率之和,1+1+1+2/3+2/3+2/4=29/6。这其实是与期望值相关的一个公式。
同样地,如果有n个数,那么快速排序平均需要的比较次数可以写成下面的式子。令k=j-i,我们能够最终得到比较次数的期望值为O(nlogn)。
这里用到了一个知识:1+1/2+1/3+...+1/n与log n增长速度相同,即∑(1/n)=Θ(log n)。它的证明放在本文的最后。
在三种O(nlogn)的排序算法中,快速排序的理论复杂度最不理想,除了它以外今天说的另外两种算法都是以最坏情况O(nlogn)的复杂度进行排序。但实践上看快速排序效率最高(不然为啥叫快速排序呢),原因在于快速排序的代码比其它同复杂度的算法更简洁,常数时间更小。
快速排序也有一个有趣的副产品:快速选择给出的一些数中第k小的数。一种简单的方法是使用上述任一种O(nlogn)的算法对这些数进行排序并返回排序后数组的第k个元素。快速选择(Quick Select)算法可以在平均O(n)的时间完成这一操作。它的最坏情况同快速排序一样,也是O(n^2)。在每一次分割后,我们都可以知道比关键字小的数有多少个,从而确定了关键字在所有数中是第几小的。我们假设关键字是第m小。如果k=m,那么我们就找到了答案——第k小元素即该关键字。否则,我们递归地计算左边或者右边:当k<m时,我们递归地寻找左边的元素中第k小的;当k>m时,我们递归地寻找右边的元素中第k-m小的数。由于我们不考虑所有的数的顺序,只需要递归其中的一边,因此复杂度大大降低。复杂度平均线性,我们不再具体证了。
还有一种算法可以在最坏O(n)的时间里找出第k小元素。那是我见过的所有算法中最没有实用价值的算法。那个O(n)只有理论价值。
============================华丽的分割线============================
我们前面证明过,仅仅依靠交换相邻元素的操作,复杂度只能达到O(n^2)。于是,人们尝试交换距离更远的元素。当人们发现O(nlogn)的排序算法似乎已经是极限的时候,又是什么制约了复杂度的下界呢?我们将要讨论的是更底层的东西。我们仍然假设所有的数都不相等。
我们总是不断在数与数之间进行比较。你可以试试,只用4次比较绝对不可能给4个数排出顺序。每多进行一次比较我们就又多知道了一个大小关系,从4次比较中一共可以获知4个大小关系。4个大小关系共有2^4=16种组合方式,而4个数的顺序一共有4!=24种。也就是说,4次比较可能出现的结果数目不足以区分24种可能的顺序。更一般地,给你n个数叫你排序,可能的答案共有n!个,k次比较只能区分2^k种可能,于是只有2^k>=n!时才有可能排出顺序。等号两边取对数,于是,给n个数排序至少需要log2(n!)次。注意,我们并没有说明一定能通过log2(n!)次比较排出顺序。虽然2^5=32超过了4!,但这不足以说明5次比较一定足够。如何用5次比较确定4个数的大小关系还需要进一步研究。第一次例外发生在n=12的时候,虽然2^29>12!,但现已证明给12个数排序最少需要30次比较。我们可以证明log(n!)的增长速度与nlogn相同,即log(n!)=Θ(nlogn)。这是排序所需要的最少的比较次数,它给出了排序复杂度的一个下界。log(n!)=Θ(nlogn)的证明也附在本文最后。
这篇日志的第三题中证明log2(N)是最优时用到了几乎相同的方法。那种“用天平称出重量不同的那个球至少要称几次”一类题目也可以用这种方法来解决。事实上,这里有一整套的理论,它叫做信息论。信息论是由香农(Shannon)提出的。他用对数来表示信息量,用熵来表示可能的情况的随机性,通过运算可以知道你目前得到的信息能够怎样影响最终结果的确定。如果我们的信息量是以2为底的,那信息论就变成信息学了。从根本上说,计算机的一切信息就是以2为底的信息量(bits=binary digits),因此我们常说香农是数字通信之父。信息论和热力学关系密切,比如熵的概念是直接从热力学的熵定义引申过来的。和这个有关的东西已经严重偏题了,这里不说了,有兴趣可以去看《信息论与编码理论》。我对这个也很有兴趣,半懂不懂的,很想了解更多的东西,有兴趣的同志不妨加入讨论。物理学真的很神奇,利用物理学可以解决很多纯数学问题,我有时间的话可以举一些例子。我他妈的为啥要选文科呢。
后面将介绍的三种排序是线性时间复杂度,因为,它们排序时根本不是通过互相比较来确定大小关系的。
附1:∑(1/n)=Θ(log n)的证明
首先我们证明,∑(1/n)=O(log n)。在式子1+1/2+1/3+1/4+1/5+...中,我们把1/3变成1/2,使得两个1/2加起来凑成一个1;再把1/5,1/6和1/7全部变成1/4,这样四个1/4加起来又是一个1。我们把所有1/2^k的后面2^k-1项全部扩大为1/2^k,使得这2^k个分式加起来是一个1。现在,1+1/2+...+1/n里面产生了几个1呢?我们只需要看小于n的数有多少个2的幂即可。显然,经过数的扩大后原式各项总和为log n。O(logn)是∑(1/n)的复杂度上界。
然后我们证明,∑(1/n)=Ω(log n)。在式子1+1/2+1/3+1/4+1/5+...中,我们把1/3变成1/4,使得两个1/4加起来凑成一个1/2;再把1/5,1/6和1/7全部变成1/8,这样四个1/8加起来又是一个1/2。我们把所有1/2^k的前面2^k-1项全部缩小为1/2^k,使得这2^k个分式加起来是一个1/2。现在,1+1/2+...+1/n里面产生了几个1/2呢?我们只需要看小于n的数有多少个2的幂即可。显然,经过数的缩小后原式各项总和为1/2*logn。Ω(logn)是∑(1/n)的复杂度下界。
附2:log(n!)=Θ(nlogn)的证明
首先我们证明,log(n!)=O(nlogn)。显然n!<n^n,两边取对数我们得到log(n!)<log(n^n),而log(n^n)就等于nlogn。因此,O(nlogn)是log(n!)的复杂度上界。
然后我们证明,log(n!)=Ω(nlogn)。n!=n(n-1)(n-2)(n-3)....1,把前面一半的因子全部缩小到n/2,后面一半因子全部舍去,显然有n!>(n/2)^(n/2)。两边取对数,log(n!)>(n/2)log(n/2),后者即Ω(nlogn)。因此,Ω(nlogn)是log(n!)的复杂度下界。
今天写到这里了,大家帮忙校对哦
Matrix67原创
转贴请注明出处
③ 第三章 处理机的调度与死锁
在多道程序系统中,调度的实质是一种资源分配,处理机调度是对处理机资源进行分配。处理机调度算法是指根据处理机分配策略所规定的处理机分配算法。
在多道程序系统中,进程的数量远远多于处理机的个数,因此进程争用处理机的情况在所难免。处理机调度是对处理机进行分配,即从就绪队列中按照一定的算法(公平、高效)选择一个进程并将处理机分配给它运行,以实现进程的并发执行。
高级调度又称 长程调度 或 作业调度 ,它调度的对象是 作业 。其主要功能是根据某种算法,决定将外存上处于后备队列中的哪几个作业调入内存,为它们创建进程、分配必要的资源,并将其放入就绪队列。
高级调度主要用于多道批处理系统中,在分时系统和实时系统中不设置高级调度。高级调度的执行频率较低,通常为几分钟一次。
低级调度又称为 进程调度 或 短程调度 ,其所调度的对象是 进程 或 内核级线程 。其主要功能是根据某种算法决定就绪队列中哪个进程应获得处理机,并由分派程序将处理机分配给被选中的进程。
进程调度是最基本的一种调度,在多道批处理、分时系统和实时系统三种类型的OS中都必须设置这种调度。进程调度的频率很高,一般几十毫秒一次。
中级调度又称 内存调度 。引入中级调度的目的主要是提高内存的吞吐率和系统吞吐量。为此应把那些暂时不能运行的进程调至外存等待,此时进程的状态称为 就绪驻外存状态(或挂起状态) 。当它们已具备运行条件且内存又稍有空闲时,由中级调度来决定把外存上那些已具备运行条件的就绪进程再重新调入内存,并修改其状态为就绪状态,挂在就绪队列上等待。中级调度实际上就是存储器管理中的对换功能。
为了管理和调度作业,在多道批处理系统中,为每个作业设置一个作业控制块JCB,它是作业在系统中存在的标志,其中保存了系统对作业进行管理和调度所需的全部信息,例如 作业标识 、 用户名称 、 用户账号 、 作业类型(CPU繁忙型、I/O繁忙型、批量型、终端型) 、 作业状态 、 调度信息(优先级、作业运行时间) 、 资源需求(预计运行时间、要求内存大小等) 、 资源使用情况 等。
每当一个作业进入系统时,由“作业注册”程序为该作业建立一个JCB,再根据作业类型放到相应的后备队列中等待调度。调度程序根据一定的调度算法来调度它们,被调度的作业将被装入内存。在作业运行期间,系统根据JCB中的信息和作业说明书对作业进行控制。当一个作业执行完毕后,系统负责回收分配给它的资源,并撤销该JCB。
作业调度的主要任务是 根据JCB中的信息,检查系统中的资源是否满足作业对资源的需求,以及按照一定的调度算法,从外存的后备队列中选取某些作业调入内存,并为它们创建进程、分配必要的资源,然后将新创建的进程排在就绪队列上等待调度 。因此也把作业调度称为“接纳调度”。在每次执行作业调度时,都需要决定 接纳多少个作业 和 接纳哪些作业 。
进程调度和切换的程序是操作系统的内核程序。请求调度的事件发生后,才可能运行进程调度程序,调度了新的就绪进程后,才会进行进程间的切换。在实际设计中,操作系统的内核程序运行时,若发生了引起进程调度的因素,也不一定能够马上进行进程调度与切换。
不能进行进程调度与切换的原因有:
若上述过程中发生了引起调度的条件,不能马上进行进程的调度与切换,应置系统的请求调度标志,直到上述过程结束后再进行相应的调度与切换。
应该进行进程的调度与切换的情况如下:
进程切换往往在调度完成后立刻发生。
进程调度方式是指 某个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要处理,即有更高优先权的进程进入就绪队列,此时应该如何分配处理机。
进程调度通常有 非剥夺调度方式 和 剥夺调度方式 两种方式。
又称 非抢占方式 ,是指当一个进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或发生某种事件而进入阻塞状态时,才把处理机分配给更为重要或紧迫的进程。在非剥夺调度方式下,一旦把CPU分配给一个进程,该进程就会保持CPU直到终止或转换到等待态。
非剥夺调度方式适合大多数的批处理系统,但不能用于分时系统和大多数的实时系统。
又称 抢占方式 ,是指当一个进程在处理机上执行时,若有某个更为重要或紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程。
采用剥夺调度方式,对提高系统吞吐率和响应效率都有明显的好处,但“剥夺”必须遵循一定的原则,主要有优先权、短进程优先、时间片原则等。
FCFS调度算法既可以用于作业调度,又可以用于进程调度。在作业调度中, 算法每次从后备队列中选择最先进入该队列的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。
在进程调度中,FCFS调度算法每次从就绪队列中选择最先进入该队列的进程,将处理机分配给它,直到进程完成或因某种原因而阻塞才释放处理机。
FCFS调度算法属于不可剥夺算法。若一个长作业先到达系统,就会使后面许多短作业等待很长时间,因此它不能作为分时系统和实时系统的主要调度策略。但它常被结合在其它调度策略中使用,如在使用优先级作为调度策略的系统中,往往对多个具有相同优先级的进程按FCFS处理。
特点:
短作业优先调度算法是对短作业优先调度的算法。短作业优先算法从后备队列中选择一个或若干估计运行时间最短的进程,将它们调入内存执行;短进程优先算法从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,直到完成或发生某事件而阻塞时,才释放处理机。
特点:
优先级调度算法又可称为优先权调度算法,既可以用于作业调度,又可以用于进程调度。该算法中的优先级用于描述作业的紧迫程度。在作业调度中,优先级调度算法每次从后备作业队列中选择优先级最高的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它。
该算法按照更高优先级的进程是否抢占正在执行的进程可分为两种:
根据进程的优先级是否改变,可将进程的优先级分为以下两种:
一般来说,进程优先级的设置可以参照以下原则:
高响应比优先调度算法主要用于作业调度,是对FCFS调度算法和SJF调度算法的一种综合平衡,同时考虑了每个作业的等待事件和估计的运行时间。在每次进行作业调度时,先计算后备队列中每个作业的响应比,从中选出响应比最高的作业投入运行。
响应比的计算公式为: 响应比Rp = (等待时间 + 要求服务时间)/ 要求服务时间
特征:
时间片轮转调度算法主要用于分时系统。系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中的第一个进程执行,即先来先服务原则,但仅能运行一个时间片(如100ms)。在使用完一个时间片后,即使进程未完成其运行,它也必须释放处理机给下一个就绪的进程,而被剥夺处理机的进程返回就绪队列的末尾重新排队,等候再次运行。
时间片的大小对性能影响很大。若时间片足够大以至于所有进程都能在一个时间片内执行完毕,则该算法就退化为先来先服务调度算法;若时间片很小,则处理机将在进程间过于频繁地切换,使处理机的开销增大,而真正用于运行用户进程的时间将减少。
时间片的长短通常由以下因素确定:
多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合与发展。多级反馈队列调度算法可以兼顾多方面的系统目标,如为提高系统吞吐量和缩短平均周转时间而照顾短进程;为获得较好的I/O设备利用率和缩短响应时间而照顾I/O型进程;不必事先估计进程的执行时间。
该算法的实现思想如下:
优点:
死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
为使系统不发生死锁,必须破坏死锁的四个必要条件之一,或允许死锁产生,但当死锁发生时能检测出死锁,并有能力实现恢复。
设置某些限制条件,破坏产生死锁的四个必要条件中的一个或几个,以防止发生死锁。
在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而避免死锁。
无需采取任何限制性措施,允许进程在运行的过程中发生死锁,通过系统的检测机构及时地检测出死锁的发生,然后采取某种措施解除死锁。
死锁的预防和避免都属于事先预防策略,预防死锁的限制条件比较严格,实现起来较为简单,但往往导致系统的效率低,资源利用率低;避免死锁的限制条件比较宽松,资源分配后需要通过算法来判断是否进入不安全状态,实现起来比较复杂。
死锁的几种处理策略如下:
防止死锁的发生只需要破坏死锁产生的4个必要条件的其中之一即可。
特点:把只能互斥使用的资源改造成允许共享使用,则系统就不会进入死锁状态。如操作系统使用SPOOLing技术把独占设备在逻辑上改造成共享设备。
缺点:并不是所有的资源都可以改造成可以共享使用的资源,并且为了系统安全,很多地方还必须保持这种互斥性。因此,很多时候都无法破坏这种互斥条件。
特点:
缺点:
特点:可采用静态分配方法,即进程运行之前一次申请完它所需要的全部资源,在它的资源尚未得到满足前,不把它投入运行。一旦投入运行,这些资源就一直归它所有,不再提出申请其他资源的请求。这种方法实现起来简单。
缺点:有些资源可能只需要使用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。另外,该策略也有可能导致某些进程饥饿。
特点:采用顺序资源分配法,首先给系统中的资源编号,规定每个进程都必须按照编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源Ri,则该进程在以后的资源申请中就只能申请编号大于Ri的资源(一个进程只有已占有我号的资源时,才有资格申请更大编号的资源)。按此规则,已持有更大编号资源的进程不可能逆向地申请我号的资源,从而就不会产生循环等待现象。
缺点:
所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要找出一个安全序列,系统就是安全状态。安全序列可能有多个。
如果分配了资源之后,系统找不到安全序列,则系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能提前回到安全状态。
如果系统处于安全状态,那么就一定不会发生死锁;如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就发生了死锁,但发生死锁时系统一定处于不安全状态)。因此可以在资源分配前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。
设Requesti是进程Pi的请求向量,Requesti[j] = K表示进程Pi需要j类资源K个。当Pi发出资源请求后,系统按照如下步骤进行检查:
安全性算法描述如下:
若系统在为进程分配资源时不采取任何措施,则应该提供死锁检测与解除的手段。
系统死锁可用资源分配图描述。如下图所示,圆圈代表一个进程,框代表一类资源,由于一种资源可能有多个,所以用框中的一个圆代表一类资源中的一个资源。从进程到资源的有向边称为请求边,表示该进程申请一个单位的该类资源;从资源到进程的边称为分配边,表示该类资源已经有一个资源分配给了该进程。
通过简化资源分配图可以检测系统状态S是否为死锁状态。简化方法如下: