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

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

多线程算法(二)

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

 

Thomas H. 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 花费单位时间的话,那么其 work 17 个单位时间,其 span 8 个单位时间。

 

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

 

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

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

 

T P T 1 /P.                                                           (27.2)

 

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

 

       T P T .                                                              (27.3)

 

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

 

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

 

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

 

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

调度

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

 

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

 

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

 

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

 

定理 27.1

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

 

T P T 1 / P + T .                                                              (27.4)

 

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

 

P*( T 1 / P +1) = P T 1 / P +

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

               > T 1                               ( 根据不等式 3.9 得出 )

 

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

 

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

 

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

 

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

 

推论 27.2

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

 

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

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

T P T 1 /P + T

2*max(T 1 /P, T )

2* T P *

 

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

 

推论 27.3

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

 

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

 

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

多线程算法分析

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

       分析 P-FIB(n) work T 1 (n) 没什么难度,因为我们已经做过了。原始的 FIB 过程就是 P-FIB 的串行化版本,因此 T 1 (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) parallelism T 1 (n)/ T (n) = Θ( Φn /n) ,其随着n 增长的速度极快。因此,对 P-FIB(n) 来说, 即使在最大的并行计算机上,一个中等大小的n 值就足以获得接近完全的线性加速,因为该过程具有相当大的并行 slackness

并行循环

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

 

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

 

y i = n j=1 a ij x j

 

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

MAT-VEC(A,x)

1         n = A.rows

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

3         parallel for i = 1 to n

4              y i = 0

5         parallel for i = 1 to n

6             for j = 1 to n

7                y i = y i + a ij x j

8         return y

 

       在这段代码中,第 3 行和第 5 行中的 parallel for 关键字表示着这两个循环中的迭代都可以并发执行。编译器可以把 parallel for 循环实现为基于嵌套并行的分治式子例程。例如,第 5 7 行中的 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                    for j = 1 to n

3                        y i = y i + a ij x j

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

 

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

 

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

T (n)= Θ(lgn) + max1 in iter (i)

 

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

条件竞争(待续)


  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值