算法导论第 3 版之多线程算法(二)

来源:互联网 发布:java syslog发送 编辑:程序博客网 时间:2024/06/06 20:37

用于学习和交流,欢迎指正。

多线程算法(二)

                                                                                      ——算法导论第3版新增第27

 

ThomasH. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein

 

邓辉

 

原文:http://software.intel.com/sites/products/documentation/cilk/book_chapter.pdf

 

       本书中的主要算法都是顺序算法,适合于运行在每次只能执行一条指令的单处理器计算机上。在本章中,我们要把算法模型转向并行算法,它们可以运行在能够同时执行多条指令的多处理器计算机中。我们将着重探索优雅的动态多线程算法模型,该模型既有助于算法的设计和分析,同时也易于进行高效的实现。<!-- /* Font Definitions */ @font-face{font-family:Wingdings;panose-1:5 0 0 0 0 0 0 0 0 0;mso-font-charset:2;mso-generic-font-family:auto;mso-font-pitch:variable;mso-font-signature:0 268435456 0 0 -2147483648 0;}@font-face{font-family:宋体;panose-1:2 1 6 0 3 1 1 1 1 1;mso-font-alt:SimSun;mso-font-charset:134;mso-generic-font-family:auto;mso-font-pitch:variable;mso-font-signature:3 135135232 16 0 262145 0;}@font-face{font-family:Times-Roman;panose-1:0 0 0 0 0 0 0 0 0 0;mso-font-alt:"Times New Roman";mso-font-charset:0;mso-generic-font-family:roman;mso-font-format:other;mso-font-pitch:auto;mso-font-signature:3 0 0 0 1 0;}@font-face{font-family:"/@宋体";panose-1:2 1 6 0 3 1 1 1 1 1;mso-font-charset:134;mso-generic-font-family:auto;mso-font-pitch:variable;mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal{mso-style-parent:"";margin:0pt;margin-bottom:.0001pt;text-align:justify;text-justify:inter-ideograph;mso-pagination:none;font-size:10.5pt;mso-bidi-font-size:12.0pt;font-family:"Times New Roman";mso-fareast-font-family:宋体;mso-font-kerning:1.0pt;}h3{mso-style-next:正文;margin-top:13.0pt;margin-right:0pt;margin-bottom:13.0pt;margin-left:0pt;text-align:justify;text-justify:inter-ideograph;line-height:173%;mso-pagination:lines-together;page-break-after:avoid;mso-outline-level:3;font-size:16.0pt;font-family:"Times New Roman";mso-font-kerning:1.0pt;} /* Page Definitions */ @page{mso-page-border-surround-header:no;mso-page-border-surround-footer:no;}@page Section1{size:612.0pt 792.0pt;margin:72.0pt 90.0pt 72.0pt 90.0pt;mso-header-margin:36.0pt;mso-footer-margin:36.0pt;mso-paper-source:0;}div.Section1{page:Section1;} /* List Definitions */ @list l0{mso-list-id:203835093;mso-list-type:hybrid;mso-list-template-ids:1293337956 215491470 67698713 67698715 67698703 67698713 67698715 67698703 67698713 67698715;}@list l0:level1{mso-level-text:%1;mso-level-tab-stop:27.0pt;mso-level-number-position:left;margin-left:27.0pt;text-indent:-27.0pt;}@list l1{mso-list-id:1578057418;mso-list-type:hybrid;mso-list-template-ids:-511670250 67698689 67698691 67698693 67698689 67698691 67698693 67698689 67698691 67698693;}@list l1:level1{mso-level-number-format:bullet;mso-level-text:;mso-level-tab-stop:21.0pt;mso-level-number-position:left;margin-left:21.0pt;text-indent:-21.0pt;font-family:Wingdings;}@list l2{mso-list-id:1968582589;mso-list-type:hybrid;mso-list-template-ids:1847216626 435871226 67698713 67698715 67698703 67698713 67698715 67698703 67698713 67698715;}@list l2:level1{mso-level-text:%1;mso-level-tab-stop:18.0pt;mso-level-number-position:left;margin-left:18.0pt;text-indent:-18.0pt;}ol{margin-bottom:0pt;}ul{margin-bottom:0pt;}-->

性能度量

       我们可以使用两个度量:“work”和“span”,来衡量多线程算法的理论效率。work指的是在一个处理器上完成全部的计算所需要的总时间。也就是说,work是所有strand执行时间的总和。如果计算dag中每个strand都花费单位时间,那么其work就是dag中顶点的数目。span是在沿dag中任意路径执行strand所花费的最长时间。同样,如果dag中每个strand都花费单位时间,那么其span就等于dag中最长路径(也就是关键路径)上顶点的数目。(在24.2节中讲过,可以在Θ(V+E)时间内找到dag G=(V,E)的一条关键路径)。例如,图27.2中的计算dag共有17个顶点,其中8个在关键路径上,因此,如果每个strand花费单位时间的话,那么其work17个单位时间,其span8个单位时间。

 

       多线程计算的实际运行时间不仅依赖于其workspan,还和可用处理器的数目以及调度器向处理器分配strand的策略有关。我们用下标P来表示一个在P个处理器上的多线程计算的运行时间。比如,我们用TP来表示算法在P个处理器上的运行时间。work就是在一个处理器上的运行时间,也就是T1span就是每个strand具有自己独立处理器时的运行时间(也就是说,如果可用的处理器数目是无限的),用T来表示。

 

workspan提供了在P个处理器上运行的多线程计算花费时间TP的下界:

l        在一个单位时间中,具有P个处理器的理想并行计算机最多能够完成P个单位工作,因此在TP时间内,能够完成最多PTP数量的工作。由于总的工作为T1,因此我们有:PTP T1。两边同除以P得到work法则(work law

 

TP T1/P.                                                          (27.2)

 

l        具有P个处理器的理想并行计算机肯定无法快过具有无限数量处理器的机器。换种说法,具有无限数量处理器的机器可以通过仅使用P个处理器的方法来仿真具有P个处理器的机器。因此,得到span法则(spaw law

 

       TP T.                                                            (27.3)

 

       我们用比率T1/ TP来定义在P个处理器上一个计算的加速因子(speedup,它表示该计算在P个处理器上比在1个处理器上快多少倍。根据work法则,TP T1/P,意味着T1/TPP。因此,在P个处理器上的加速因子最多为P。当加速因子和处理器的数目成线性关系时,也就是说,当T1/TP=ΘP时,该计算具有线性加速的性质,当T1/TP=P时,称其为完全的线性加速

 

       我们把workspan的比率T1/T定义为多线程计算的parallelism(并行度)。可以从三个角度来理解parallelism。作为一个比率,parallelism表示了对于关键路径上的每一步,能够并行执行的平均工作量。作为一个上限,parallelism给出了在具有任何数量处理器的机器上,能达到的最大可能加速。最后,也是最重要的,在达成完全线性加速的可能性上,parallelism提供了一个在限制。具体地说,就是一旦处理器的数目超过了parallelism,那么计算就不可能达成完全线性加速。为了说明最后一点,我们假设P > T1/T,根据span法则,加速因子满足T1/TPT1/T<P。此外,如果理想并行计算机的处理器数目P大大超过了parallelism(也就是说,如果P >> T1/T),那么T1/TP<<P,这样,加速因子就远小于处理器的数目。换句话说,处理器的数目超过parallelism越多,就越无法达成完全加速。

 

       例如,我们来看看图27.2P-FIB(4)的计算过程,并假设每个strand花费单位时间。由于work T1=17span T=8,因此parallelism T1/T=17/8=2.125。从而,无论我们用多少处理器来执行该计算,都无法获得2倍以上的加速因子。不过,对于更大一些的输入来说,P-FIB(n)会呈现出更大的parallelism

 

       我们把在一台具有P个处理器的理想并行计算机上执行多线程算法的并行slackness(闲置因子)定义为:(T1/T)/P = T1/(PT),也就是计算的parallelism超过机器处理器数目的倍数因子。因此,如果slackness小于1,那么就不能达成完全的线性加速,因为T1/(PT)<1,根据span法则,在P个处理器上的加速因子满足T1/TPT1/T<P。事实上,随着slackness1降低到0,计算的加速因子就越来越远离完全线性加速。如果slackness大于1,那么单个处理器上工作量就成为限制约束。我们将看到,随着slackness1开始增加,一个好的调度器可以越来越接近于完全线性加速。

调度

       好的性能并不仅仅来自于对workspan的最小化,还必须能够高效地把strands调度到并行计算机的处理器上。我们的多线程编程模型中没有提供指定哪些strands运行在哪些处理器上的方法。而是依赖于并发平台的调度器来把动态展开的计算映射到单独的处理器上。事实上,调度器只把strands映射到静态线程,由操作系统来把线程调度到处理器上,不过这个额外的间接层次并不是理解调度原理所必需的。我们可以就认为是由并发平台的调度器直接把strands映射到处理器的。

 

       多线程调度器必须能够在事先不知道strands何时被spawn以及何时完成的情况下进行计算的调度——它必须在线(on-line操作。此外,一个好的调度器是以分散的(distributed)形式运转的,其中实现调度器的线程互相协作以均衡计算负载。好的在线、分散式调度器确实存在,不过对它们进行分析是非常困难的。

 

       因此,为了简化分析工作,我们将研究一个在线、集中式(centralized调度器,在任意时刻,它都知道计算的全局状态。我们将特别分析贪婪式调度器,它们会在每个执行步骤中把尽可能多的strands分配给处理器。如果在一个执行步骤中有至少Pstrands可以执行,那么就称这个步骤为完全步骤,贪婪调度器会把就绪strands中的任意P个分配给处理器。否则,如果就绪的strands少于P个,则称这个步骤为不完全步骤,调度器会把每个strand分配给独立的处理器。

 

       根据work法则,在P个处理器上可以达到的最快运行时间为TP= T1/P,根据span法则,最好的情况是TP=T。下面的定理表明,因为贪婪式调度器可以以这两个下界之和为其上界,所以其可被证明是一个好的调度器。

 

定理27.1

       在一台具有P个处理器的理想并行计算机上,对于一个wrokT1spanT的多线程计算,贪婪调度器执行该计算的时间为:

 

TPT1/P + T.                                                             (27.4)

 

证明:首先来考虑完全步骤。在每个完全步骤中,P个处理器完成的工作总量为P。我们采用反证法,假设完全步骤的数目严格大于T1/P┘,那么完全步骤所完成的工作总量至少为:

 

P*(T1/P+1) = PT1/P +

        =  T1-(T1 mod P+ P (根据等式3.8得出)

               > T1                               (根据不等式3.9得出)

 

因此,P个处理器所完成的工作比所需要的还多,矛盾,所以完全步骤的数目最多为T1/P┘。

 

    现在,考虑一个不完全步骤。我们用G来表示整个计算的dag,不失一般性,假设每个strand都花费单位时间。(我们可以把超过单位时间的strand用一串单位时间strand来替代)。令G’为在该不完全步骤开始时G已经执行的部分构成的子图,令G”为在该不完全步骤完成后G中还没有执行的部分构成的子图。dag中最长的路径一定起始于入度(in-degree)为0的顶点。由于贪婪调度器中的一个不完全步骤会把G’中所有入度为0strands全部执行,因此G”的最长路径长度一定不G’中的最长路径小1。换句话说,一个不完全步骤会把还没有执行的dagspan1。所以,非完全步骤的数目最多为T

 

       由于每个步骤要么是完全的,要么是不完全的,因此定理得证。

 

       下面是定理27.1的推论,说明了贪婪式调度器总是具有好的调度性能。

 

推论27.2

在一台具有P个处理器的理想并行计算机上,任何由贪婪式调度器调度的多线程计算的运行时间TP,不会超过最优时间的2倍。

 

证明:TP*为在具有P个处理器的机器上,一个最优调度器产生的运行时间,令T1T为该计算的workspan。根据work法则和span法则(不等式27.227.3),得出:

TP*max(T1/P, T),根据定理27.1,有:

TP T1/P + T

2*max(T1/P, T)

2* TP*

 

              下一个推论告诉我们,对于任何多线程计算来所,随着slackness的增长,贪婪式调度器都可以达到接近完全的线性加速。

 

推论27.3

TP为在一台具有P个处理器的理想并行计算机上,贪婪式调度器调度一个多线程计算的运行时间,令T1T为该计算的workspan。那么如果P << T1/T,就有TPT1/P(或者相等),也就是具有大约为P的加速因子。

 

证明:假设P<< T1/T,那么就有T<< T1/P,因此根据定理27.1,有TPT1/P + T。根据work法则(27.2)得到TPT1/P,因此得出TPT1/P(或者相等),加速因子为:T1/TPP

 

       符号<<表示“远小于”,但是“远小于”意味着多少呢?作为经验之谈,当slackness至少为10时(也就是说,parallelism是处理器数目的10倍),通常就足以得到很高的加速因子。贪婪调度器的上界不等式(27.4)中的span项小于单处理器work项的10%,这对于绝大多数实际应用情况而言已经足够好了。例如,如果一个计算仅在10个或者1000个处理器上运行,那么去说1,000,000parallelism10,000更好是没有意义的,即使它们之间有100倍的差异。正如问题27-2所表明的那样,有时通过降低计算的最大并行度,所得到的算法要好于关注其他问题所得到算法,并且还能在相当数目的处理器上伸缩良好。

多线程算法分析

       现在,我们已经拥有了分析多线程算法的所有工具,并且对于在不同数目处理器上的运行时间也有了个不错的边界。对于work的分析相对简单,因为只不过就是分析一个普通的串行算法的运行时间(也就是多线程算法的串行化版本),对此,我们早已熟悉,这正是本书大部分内容所讲的东西!对span的分析会更有趣一些,一旦掌握了其中的诀窍,通常也不难。我们将以P-FIB程序为例来研究一些基本概念。

       分析P-FIB(n)work T1(n)没什么难度,因为我们已经做过了。原始的FIB过程就是P-FIB的串行化版本,因此T1(n)=T(n)= Θ(Φn)(基于等式27.1)。

 

    27.3中展示了如何去分析span。如果两个子计算被串行合并在一起,那么其组合的span等于二者span之和,如果它们被并行合并在一起,那么其组合的span等于二者span中较大的那一个。对于P-FIB(n)来说,第3行中spawnP-FIB(n-1)和第4行中spawnP-FIB(n-2)并行运行。因此,我们可以把P-FIB(n)span表示为如下递归式:

T (n) = max(T(n-1), T (n-2)) +Θ(1)

     = T(n-1) +Θ(1),

结果为:T(n) = Θ(n)

 

    P-FIB(n)parallelismT1 (n)/ T (n) =Θ(Φn/n),其随着n增长的速度极快。因此,对P-FIB(n)来说,即使在最大的并行计算机上,一个中等大小的n值就足以获得接近完全的线性加速,因为该过程具有相当大的并行slackness

并行循环

       有许多算法,其包含的循环中的所有迭代都可以并行执行。我们将看到,可以实用spawnsync关键字来并行化这种循环,不过如果能够直接指明这种循环的迭代可以并发执行的话,会更加方便一些。我们通过使用parallel并发关键字来在伪码中提供该功能,它位于for循环语句的for关键字之前。

 

       我们以一个n×n的矩阵A=aij)乘以一个n元向量x=xj)为例进行说明。相乘的结果为一个n元向量y=yi),如下:

 

yi = nj=1aij xj

 

i=12…,n。我们可以通过并行地计算y的所有项来进行矩阵-向量的乘法操作,如下:

MAT-VEC(A,x)

1        n = A.rows

2        y为一个新的长度为n的向量

3        parallel for i = 1 to n

4            yi = 0

5        parallel for i = 1 to n

6           for j = 1 to n

7              yi = yi + aij xj

8        return y

 

       在这段代码中,第3行和第5行中的parallel for关键字表示着这两个循环中的迭代都可以并发执行。编译器可以把parallel for循环实现为基于嵌套并行的分治式子例程。例如,第57行中的parallel for循环可以被实现为对MAT-VEC-MAIN-LOOP(A,x,y,n,l,n)的调用,子例程MAT-VEC-MAIN-LOOP是编译器生成的辅助子例程,如下:

MAT-VEC-MAIN-LOOP(A, x, y, n, i, i’)

1              if i == i’

2                  forj = 1 to n

3                      yi = yi + aij xj

4              else mid = (i+i’)/2

5                  spawn MAT-VEC-MAIN-LOOP(A, x, y, n, i,mid)

6                  MAT-VEC-MAIN-LOOP(A, x, y, n, mid+1, i’)

7                  sync

 

       该代码递归地spawn循环中的前半部分迭代,使其和后半部分迭代并行执行,然后执行一条sync语句,创建了一棵二叉树式的执行过程,其中叶子为单独的循环迭代,如图27.4所示。

       现在来计算对于n×n矩阵,MAT-VECwork T1(n),也就是计算其串行化版本的运行时间,这个串行化版本可以通过把parallel for循环替换成普通的for循环得到。由此,我们得到T1(n)= Θ(n2),因为第57行的两重嵌套循环所产生的平方级运行时间占支配地位。在这个分析中,我们忽略掉了实现并行循环的递归spawn的开销。事实上,和其串行化版本相比,递归spawn的开销确实增加了并行循环的工作量,不过并不是渐进关系的。原因如下,因为递归过程实例树是一颗满二叉树,所以内部节点的个数正好比叶子的个数少1(见练习B.5-3)。每个内部节点分割迭代范围时所耗费的都是常数时间,并且每个叶子都对应循环中的一个迭代,其至少耗费常数时间(在本例中是Θ(n))。因此,我们可以把递归spawn的开销分摊到迭代的工作中,对全部工作来说,至多增加了一个常数倍数因此。

 

       在实际实现中,动态多线程并发平台时常会在一个叶子中执行多个迭代,从而使得递归产生的叶子的粒度变粗,这个过程可以是自动地,也可以由程序员来控制,因此减少了递归spawn的开销。付出的代价是降低了并行度,不过,如果计算具有局够大的并行slackness,那么还是可以达成接近完全的线性加速的。

 

       在分析并行循环的span时,也必须得考虑到递归spawn的开销。由于递归调用的深度和迭代的次数成对数关系,因此对于一个具有n次迭代,其第i个迭代的spaniter (i)的并行循环来说,其span为:

T(n)= Θ(lgn)+ max1in iter (i)

 

例如,对于以一个n×n矩阵为参数的MAT-VEC来说,第34行中的并行初始化循环的spanΘ(lgn),因为和每个迭代中的常数工作时间相比,递归spawn占支配地位。第57行中的双重嵌套循环的span为Θ(n),因为外层parallel for循环的每个迭代都包含着内层(串行)for循环的n个迭代。伪码中剩余部分的span为常数,因此整个过程的span由双重嵌套循环支配,也就是Θ(n)。由于过程的workΘ(n2),所以parallelismΘ(n2)/ Θ(n)  =Θ(n)。(练习27.1-6会让读者提供一个具有更高并行度的实现)。

条件竞争(待续)


原创粉丝点击