8.2 ON指令  BACKWARDFORWARD


ON指令的目的是允许程序员控制并行机处理器上的计算分配。在某种意义上,它与DISTRIBUTE和ALIGN指令类似,所不同的是前者用于计算,而后者用于数据。ON指令是通过为语句或语句组指定活动处理器集合来做到这一点的。这样做临时缩小了活动处理器集合并给程序员以更大的控制权。如果两个ON块中的计算是无关的(例如,如果这两个ON块是在INDEPENDENT循环的两个迭代中),则它们为编译器开发并行性提供了清晰的指令。

8.2.1 ON指令的语法

ON指令有两种形式:单语句形式和多语句形式。这些指令的BNF是:

H802 simple-on-directive is ON home-expr [ , resident-clause ] [, new-clause]
H803 block-on-directive is simple-on-directive BEGIN
H804 on-block       is
               block-on-directive
               block
               end-directive
H805 end-on-directive   is END [ ON ]
H806 home-expr      is HOME( variable )
              or HOME( template-elmt )
              or ( processors-elmt )
H807 template-elmt    is template-name [ ( section-subscript-list) ]
H808 processors-elmt   is processors-name [ ( section-subscript-list) ] 

resident-clause将在8.3节中定义。当出现这一子句时,就足以说明这是一个简介中所提及的RESIDENT指令形式。

home-expr,template-elmt和processors-elmt是附属的语法种类。即使在不使用关键字HOME时,home-expr也通常被称作HOME子句。注意,变量(variable)是一个Fortran语法术语,它指的是“一个引用,包括一个数组元素,数组区域,或派生类型域”;变量不包括模板或处理器元素,这是因为它们都不是第一类的语言结构。还要注意,块(block)是一个Fortran语法术语,指的是“被看作一个组的一系列语句”—例如DO结构体。

simple-on-directive是executable-directive下的一个选项(见规则H105)。这意味着simple-on-directive可出现在可执行语句所出现的任何地方。

on-block是一个Fortran可执行语句。这个语法表示块可以是嵌套的,并且如果这样的话,它们将被正确地嵌套。

基本原理:注意home-expr规则的最后一个选项中的括号的使用(涉及processors-elmt)。这样就防止了下列二义性:

    INTEGER X(4) ! X(I) 将在处理器I上
 !HPF$ PROCESSORS HOME(4)
 !HPF$ DISTRIBUTE X(BLOCK)
    X = (/ 4,3,2,1 /)
 !HPF$ ON HOME(X(2))
    X(2) = X(1)

如果不要求括号,则计算应在那里进行?

  1. 处理器HOME(2)(例如是X(2)的拥有者)?
  2. 处理器HOME(3)(例如在赋值前使用X(2)的值)?
  3. 处理器HOME(4)(例如在赋值后使用X(2)的值)?

ON的定义清晰地指明了解释1是正确的。我们可通过下面的指令得到解释2的结果:

 !HPF$ ON(HOME(X(2)))

没有任何方法能得到解释3的结果,原因是缺少一个有洞察力的计算机。解决这一问题的一个比较好的建议是向Fortran中引入保留关键字,但是我们也看到这样做对基本语言改动太大。

(基本原理结束)

8.2.2 ON指令的语义

ON指令将进行一个计算的活动处理器集合限制为它的home-expr中所命名的那些处理器。即它建议编译器使用所命名的处理器来执行计算。类似于ALIGN和DISTRIBUTE,这只是一个建议而不是一个纯粹的命令;编译器可以不理会ON指令。同ALIGN和DISTRIBUTE的另一个相似点是,ON指令可以影响计算的效率,但不会影响最终的结果。

对实现者的建议:如果编译器可以不理会ON指令中用户的建议,则编译器也应该为用户提供一个选项以便能够强迫所有的指令被遵守。(对实现者的建议结束)

单语句ON指令(例如simple-on-directive)为紧接它的第一条非注释语句设置了活动处理器集合。可以认为ON指令适用于该语句。如果该语句是一个复合语句(例如一个DO循环或一个IF-THEN-ELSE结构),则ON指令还适用于它里面的所有嵌套语句。类似地,块ON指令(例如on-block)将初始ON子句应用于(例如,设置活动处理器集合)直到对应END指令之前的所有语句。

HOME子句可为一个程序变量,模板,或处理器排列命名。对于这些可能性的每一个,它都可以指定单一一个元素或多个元素。这样做所翻译成的执行计算的处理器如下所示:

home-expr中表达式值的确定就好象它们的估算是在控制流到达ON指令时。如果HOME子句中表达式的估算要改变程序中任意变量的值,则该变量的值在到达ON子句后就变成未定义的。

无论哪种情况中,ON指令都指定应该执行计算的处理器。更正规地,它为计算设置活动处理器,见8.1节中的描述。该节中还描述了一些语句(著名的ALLOCATE和动态映射指令)是怎样要求特定的处理器被包含在活动集合中的。如果这些结构之一出现在ON块中且活动处理器集合不够用,则程序不符合标准。注意这里仅指定了计算是怎样在处理器之间被划分的;并没有指明那些可能涉及数据传输的处理器。另外,ON指令本身并不保证它自己可以与任意其它操作并行执行。但是,安放计算能够对数据局部性产生很重要的影响。如后面的例子中所显示的那样,ON和INDEPENDENT组合在一起还可以控制负载平衡并行计算。

基本原理:这里是ON子句的核心。它定义了计算在哪里执行,但没有指明什么数据被访问。

“好象”这个词以及使副作用变成未定义避免了下面的问题:ON是一个指令。因此它不能具有副作用。但是实现ON子句时可能需要估算一些函数,因此会产生副作用。而且不同的系统可能使用不同的机制。通过使它们的结果变得未定义,剥夺了这种对于实现机制的依赖。

(基本原理结束)

对实现者的建议:如果HPF程序被编译成SPMD代码,则通过让所有的处理器将它们的处理器id与HOME子句所产生的id(或id列表)相对照,总能实现ON子句(虽然会很低效)。(在其它的模式中也可以构造类似地朴素实现。)如果ON子句被重复执行,例如在DO循环中,则对这一处理进行转换是很有价值的。即,不考虑执行所有HOME子句测试的所有处理器,编译器应该能够确定在本地处理器上测试为真的循环迭代的范围。(进一步的细节见8.2.3节中“对用户的建议”。)例如,考虑下面的复杂情况:

 DO I=1,N
  !HPF$ ON HOME(A(MY_FCN(I))) BEGIN
    ...
  !HPF$ END ON
 END DO

这里,所产生的代码可以执行一个“视察者”(例如仅估算每次迭代HOME子句的一个概略循环)以产生赋给每个处理器的一个迭代列表。由于MY_FN必须没有副作用(至少程序远不能依赖于任何副作用),因此此列表可以并行产生。但是,向所有的处理器分配home-expr计算可能需要非结构化的通信模式,因此可能使得并行的优点被否定。一般来说,更高级的编译器能够有效的转换更复杂的HOME子句。建议向用户清楚地说明特定编译器的能力(和限制)。

注意由朴素实现所筛选的处理器可能仍然需要参加数据传输。如果基础的结构允许单向通信(例如,共享存储或GET/PUT),这就不是一个问题。对于消息传递机器,需要使用一个应答协议。这就要求非活动处理器直到ON块完成才能进入等待循环,或者要求运行时系统异步处理请求。另外推荐文档告诉程序员在一个特定系统上哪种情况可能有效,那种低效。(对实现者的建议结束)

对用户的建议:ON指令中home-expr的形式可以是任意复杂的。它可以解释非常复杂的计算划分,但是这些划分的实现可能效率不高。更具体地,它可以解释一个完善的负载平衡计算,但是要强制编译器将计算串行化以实现HOME子句。虽然ON子句的开销数随HPF代码,编译器和硬件的不同而不同,但人们仍然期望编译器能够仅基于数组映射或一个命名处理器排列就产生好的代码,而且随home-expr复杂性的增长代码是渐次变坏的。ON指令复杂性的一个粗略估计是用于计算的运行时数据的数目;例如,一个常数偏移相当简单,而一个置换数组就非常复杂。8.2.3节给出了这一现象的更具体的例子。

仅仅ON子句不产生数据移动没有任何意义。(8.3节的RESIDENT指令就做到了这一点)因此,在一些机器上,一些额外的处理器不得不进入ON块来参加通信。

还应注意到ON子句不改变程序语义,同样意义上,DISTRIBUTE也不改变语义。特别是由于ON块内的代码仍能与ON块外的代码相互影响,因此ON指令不能将串行代码转变为并行代码。(ON也不派生进程)(对用户的建议结束)

如果内层ON指令所命名的活动处理器集合包含在外层指令所命名的活动处理器集合中,则嵌套ON指令是合法的。on-block自动保证它可以正确地嵌套在其它复合语句中,且该复合语句也能正确嵌套在它的里面。同其它Fortran复合语句一样,禁止将控制从块外转移进块内,但允许块内之间的转移。但是HPF还禁止on-block内的控制转移到on-block之外。注意这一点比普通Fortran更严格。如果ON子句是嵌套的,则最内层的home-expr有效地控制语句地执行。程序员可将这一点看作是在ON嵌套的每一层逐次限制处理器的集合;清楚地讲,最后的约束必须是最强的。另外,程序员可将这一点看作嵌套并行的fork-join方法。

基本原理:关于控制流进出ON块的约束本质上是将其限制为一个单入口单出口的区域,这样就大大地简化了语义。(基本原理结束)

如果ON指令中包含了一个NEW子句,则其意义与INDEPENDENT指令中的NEW子句相同。如果NEW变量在ON指令作用域的每个入口重新分配空间,且在ON块的出口被释放,则程序的操作将是同一个。即,NEW变量在入口处是死的(例如在ON块中要先赋值后使用),在出口处也是死的(例如除非首先重赋值,否则在ON块后不能使用)。另外,另外,无论通过REALIGN,REDISTRIBUTE,还是通过向子程序调用进行参数传递,NEW变量在ON子句作用域范围内都不能重映射。如果一个变量出现在NEW子句中,但没有满足这些条件,则程序不符合HPF标准。任意嵌套的RESIDENT指令都不考虑NEW变量,细节见8.3节。

基本原理:NEW子句提供了一个简单的方式来创建临时变量。当RESIDENT指令起作用时,这一能力尤其重要。(基本原理结束)

对实现者的建议:由于NEW变量不在ON块之外使用,因此它们在ON子句前后无须保持一致性。因此ON指令决定了在活动处理器集合的外面无须通信来实现它们。标量NEW变量应该复制在活动处理器集合上或者在活动处理器共享的存储区分配空间。注意如果ON块的多个实例有可能同时活动,则存储器不能动态分配。这类似于实现INDEPENDENT循环中NEW变量的要求。(对实现者的建议结束)

8.2.3 ON指令的例子

下面是有效的ON指令的例子。大多数例子中所阐述的风格都是程序员可能使用的,而不是一些人造的条件。为简化起见,开始的几个例子假定具有下面的数组说明:

    REAL A(N), B(N), C(N), D(N)
 !HPF$ DISTRIBUTE A(BLOCK), B(BLOCK), C(BLOCK), D(BLOCK)

一个最一般的要求是HPF应该能够控制循环迭代怎样分配到处理器上。(历史上,ON子句第一次出现执行这一作用是在Kali的FORALL结构中。)这一点可通过下面例子中的ON指令来做到:

 !HPF$ INDEPENDENT
 DO I = 2, N-1
  !HPF$ ON HOME(A(I))
  A(I) = (B(I) + B(I-1) + B(I+1))/3
 END DO

 !HPF$ INDEPENDENT
 DO J = 2, N-1
  !HPF$ ON HOME(A(J+1)) BEGIN
   A(J) = B(J+1) + C(J+1) + D(J+1)
  !HPF$ END ON
 END DO

I循环中的ON指令将循环每次迭代的活动处理器置为存储A(I)的处理器。另一方面,它还建议编译器让每个处理器都在它的A数组的本地区域上运行(B也是这样)。为引用B(I-1)和B(I+1)对于每个处理器的第一和最后的迭代必须从外面的处理器获得它们。(除边界处理器之外);注意,那些处理器在HOME子句中没有提及。J循环中的ON指令类似地为每次迭代设置活动集合,但是建议编译器进行移动计算。因此,每个处理器都要做其B,C,和D本地区域的向量和,并将结果的第一个元素存储在它左部的处理器上,结果的其它元素存处在A中。尽管非本地引用相当高,但是如果数组以CYCLIC方式分配,则这些指令仍然有效(并将非本地数据访问最小化)将毫无价值。

对实现者的建议:强烈推荐编译器用一个包含整个循环体的单ON子句来集中优化DO循环。概要地讲,代码应是:

  DO i = lb, ub, stride
   !HPF$ ON HOME(array(f(i))) BEGIN
   body
   !HPF$ END ON
  END DO

在这里,数组具有一些数据映射。假定该映射向处理器p提供元素myset(p)。(例如在BLOCK分配中,myset(p)是一个连续的整数范围。)则处理器p上产生的代码应该是:

  DO i [lb:ub:stride]f-1(myset(p))
   body
  END DO

(这一示例中没有显示出必须将通信或同步放在哪里;这些信息必须通过对程序体分析得到)而且,f可能是一个特性函数或一个具有整数系数的线性函数,这两种函数很容易转换。在最近的几次会议上可以找到通过集合来进行迭代的技术。(对实现者的建议结束)

对用户的建议:我们可以期望上面的I循环产生用于计算划分的有效代码。实际上,编译器将为每个处理器安排在数组A它自己区域上的迭代。在J循环中,由于编译器必须找到HOME子句下标函数的反函数,因此该循环要略微复杂一些。即编译器必须求解K=J+1中J,在这里K的变化范围是A的本地元素。当然在这种情况下,J=K-1;一般来说,编译器可以求出线性函数的反函数。(然而应该指出的是ALIGN和DISTRIBUTE的复杂组合可能使得K的描述很难控制,这可能增加求反处理的开销。)(对用户的建议结束)

有时将一个迭代在两个处理器间“割裂开”是有好处的。下面的情形显示了这样的一个例子:

 !HPF$ INDEPENDENT
 DO I = 2, N-1
  !HPF$ ON HOME(A(I))
  A(I) = (B(I) + B(I-1) + B(I+1))/3
  !HPF$ ON HOME C(I+1)
  C(I+1) = A(I) * D(I+1)
 END DO

在这里,循环体内两个语句的活动处理器集合是不同的。根据第一个ON子句,第一个语句中对A(I)的引用是本地的。第二个ON子句使得那里的A(I)变成非本地的(对于一些I的值)。这样做使得两个语句中数据局部性变得最大化,但是需要处理器间的数据移动。

对实现者的建议:如果循环中存在一些非嵌套的ON子句,则上面的例示须被一般化。实质上,必须为每个单独的ON子句产生其迭代范围。然后处理器将在这些范围的并集上进行迭代。由ON指令所守护的语句现在由一个显式测试来守护。概括地讲,下列代码

  DO i = lb, ub, stride
   !HPF$ ON HOME(array1(f1(i)))
   stmt_1
   !HPF$ ON HOME(array2(f2(i)))
   stmt_2
  END DO

在处理器p上变成

  set1 = [lb:ub:stride]f-1(myset1(p))
  set2 = [lb:ub:stride]f-1(myset2(p))
  DO i set1 set2
   IF (i set1) THEN
    stmt1
   END IF
   IF (i set2) THEN
    stmt2
   END IF
  END DO

在这里,myset1(p)是array1的本地集合,且myset2(p)array2的本地集合。(另外同步和通信必须由其它方式处理)可使用诸如循环分配和循环剥离这样的代码转换来去除许多情况下的测试。如果ON块之间存在数据依赖,则这些转换尤其重要。(对实现者的建议结束)

对用户的建议:象这样将一个迭代割裂开可能或者需要在运行时进行额外的测试,或者需要由编译器进行额外分析。即使编译器能够为单个ON子句产生低开销调度,将它们组合在一起就不一定是低开销了。这样局部性的收益就意义不大了,但是还存在许多情况多ON子句是有价值的。(如果一个ON块使用另一个ON块中所计算的数据,则所有这些语句就是尤其真实的。)(对用户的建议结束)

由于ON子句自然嵌套,因此可使用它们来解释沿不同维的并行性。考虑下面的例子:

  REAL X(M,M)
  !HPF$ DISTRIBUTE X(BLOCK,BLOCK)

  !HPF$ INDEPENDENT, NEW(I)
  DO J = 1, M
   !HPF$ ON HOME(X(:,J)) BEGIN
    DO I = 2, M
     !HPF$ ON HOME(X(I,J))
     X(I,J) = (X(I-1,J) + X(I,J)) / 2
    END DO
   !HPF$ END ON
  END DO

J循环每个迭代的活动处理器集合是(可能全体)处理器排列的一列。I循环将计算进一步细分,由每个处理器负责计算它所拥有的元素。许多编译器为这种简单的例子自动选择计算划分。但是,编译器可能会试图完全并行化外层循环,并在每个处理器上串行执行每个内层循环。(这样做对于一个通信速度非常快的机器很有吸引力)用户通过插入ON子句来反对这一策略,这样就用额外的局部性换取了受限制的并行性。注意,ON指令既不要求也没暗示INDEPENDENT声明。在这两个嵌套中,I循环的每个迭代都依赖于前面的迭代,但是ON指令仍然可以在处理器之间划分计算。ON指令并不自动使一个循环并行。

对实现者的建议:象上面那样“基于维”的嵌套将可能是一个一般的情形。可在每一层次上对HOME子句进行转换,从外层循环的角度可将索引看成是运行时的不变量。

对用户的建议:如果象上面的例子那样,ON指令中的HOME子句指的是处理器排列的不同维,则嵌套ON指令将有助于得到一个高效的实现。这样就使得循环层次间的相互影响变得最小化,简化了实现。(对用户的建议结束)

考虑上面例子的下列变种:

  !HPF$ DISTRIBUTE Y(BLOCK,*)

  !HPF$ INDEPENDENT, NEW(I)
  DO J = 1, M
   !HPF$ ON HOME(Y(:,J)) BEGIN
    DO I = 2, M
     !HPF$ ON HOME(Y(I,J))
     Y(I,J) = (Y(I-1,J) + Y(I,J)) / 2
    END DO
   !HPF$ END ON
  END DO

注意除数组名之外,没对ON子句作改变。除了外层ON指令将J循环的每个迭代赋给所有的处理器之外,本例的解释同上面的类似。内层ON指令再次实现了一个简单的拥有者计算规则。程序员已经指导编译器在所有的处理器之间分配了一系列的计算。有一些方案能够比并行化外层循环更有效:

  1. 由于任意处理器上所存放的只是每一列的一部分,因此并行化外层循环将产生许多非本地的引用。如果非本地引用的代价非常高(或者M很小),则这样做的开销很可能超过并行执行的任何收益。
  2. 编译器可以利用INDEPENDENT指令来避免插入任何同步。这样就允许一个自然的流水执行。对于J的一个值,处理器将执行它的I循环的部分,然后马上跳到下一个J迭代。这样第一个处理器将从J=2开始执行,而第二个处理器则为J=1接收它所需的数据(从处理器1)。(在上例的的X中可开发类似的流水线)

很显然,这些ON子句的适用性依赖于下面的并行结构。

对用户的建议:本例指出ON怎样能够提高软件管理。虽然如果X的映射改变,HOME(X(I))的值也将改变,但是它的意图通常保持不变—运行与数组X“对准”的循环。而且这些子句的形式是可扩展的,它们还同时简化了轮流试验不同计算划分的方法。这两个性质都类似于 DISTRIBUTE和ALIGN在低级数据分布机制上的优点。(对用户的建议结束)

当编译器不能精确估计数据局部性时,ON指令特别有用,例如当计算使用间接数组时。考虑同一个循环的三个变种:

  REAL X(N), Y(N)
  INTEGER IX1(M), IX2(M)
  !HPF$ DISTRIBUTE X(BLOCK), Y(BLOCK)
  !HPF$ DISTRIBUTE IX(BLOCK), IY(BLOCK)

  !HPF$ INDEPENDENT
  DO I = 1, N
   !HPF$ ON HOME( X(I) )
   X(I) = Y(IX(I)) - Y(IY(I))
  END DO

  !HPF$ INDEPENDENT
  DO J = 1, N
   !HPF$ ON HOME( IX(J) )
   X(J) = Y(IX(J)) - Y(IY(J))
  END DO

  !HPF$ INDEPENDENT
  DO K = 1, N
   !HPF$ ON HOME( X(IX(K)) )
   X(K) = Y(IX(K)) - Y(IY(K))
  END DO

在I循环中,每个处理器在它的X数组的区域上运行。(即,迭代I的活动处理器是X(I)的拥有者)仅保证引用X(I)是本地的。(如果MN,则IX和IY与X具有不同的块大小,这样就具有一个不同的映射)但是如果通常情况下X(I),Y(IX(I))和Y(IY(I))位于同一个处理器中,则活动处理器的这一选择可能是最好的。(如果X(I)和其它的一个引用总是在同一个处理器中,则程序员应该增加RESIDENT子句,见8.3节中的解释)在下一个循环中,迭代J的活动处理器是IX(J)的拥有者。因为IY与IX具有相同的分配方式,则引用IY(J)同IX(J)一样总是本地的。这是循环中最普通的数组引用类,因此它在缺少IX和IY任何特殊性质的情况下,将非本地数据的引用数最小化。它不能均衡地在处理器上装载;例如,如果N=M/2,则一半的处理器将是空闲的。同前面一样,如果IX或IY中的值保证Y的一个引用总是本地的,则应该增加RESIDENT声明。在K循环中,仅保证引用Y(IX(K))是本地的(因为X和Y具有相同的分配方式)。但是存储在IX和IY中的值可以保证Y(IY(K))和X(K)总是本地的。即使这三个REAL值不总是,但仅仅“通常”在同一处理器上,这一方案对于局部性和并行性来说都可算是一个好的计算划分。但是这些优点必须超过计算这一划分所需的开销。由于HOME子句依赖于一个(可能很大)运行时值的数组,因此可能需要很多时间来确定哪个迭代被赋给每个处理器。从这个讨论中很显然不存在一个神奇的答案来处理复杂计算划分;最好的解答通常是应用知识,认真的数据结构设计(包括元素排序),有效的编译方法和运行时支持的组合。

对实现者的建议:K循环是上面所描述的视察者策略的设计条件。如果有一个外层循环包含这些例子中的任何一个且该循环没有修改X的分配方式或者IX的值,则可保存每个处理器迭代的一个纪录以用于重使用。这一开销最坏是数组大小的线性函数。(对实现者的建议结束)

对用户的建议:当前任何编译器产品都不可能为K循环产生低开销的代码。与前面例子的不同点是HOME子句不是一个能被编译器轻易求反的函数。一些编译器可能选择在所有处理器上执行每个迭代,在运行时测试HOME子句;其它的可能为每个处理器预先计算出一个迭代列表。当然,计算这一列表的开销将是很大的。实际上,我们可以使所有的数组同样大小以避免上面的一些对准问题;本例以这种方式书写是为便于教学,不能作为一个好的数据结构设计的例子。(对用户的建议结束)

8.2.4 应用于子程序调用的ON指令

关于应用于子程序调用的ON指令的关键规则是调用不改变活动处理器集合。实际上,被调用者继承了调用者的活动处理器。这样,

  !HPF$ PROCESSORS P(10)
  !HPF$ DISTRIBUTE X(BLOCK) ONTO P

  !HPF$ ON ( P(1:3) )
  CALL PERSON_TO_PERSON()
  !HPF$ ON ( P(4:7) )
  CALL COLLECT( X )

在三个处理器上调用PERSON_TO_PERSON,而在四个处理器上调用COLLECT。传给COLLECT的实参没有完全安放在处理器的活动集合上。只要对相应哑参进行适当的说明就允许这么做,见下面的解释。

上面的规则对于调用队列中的数据分配方式具有有趣的暗示。特别是哑参的定义必须遵循与本地变量相同的约束,这样就保证了哑参总是存储在活动处理器上。但这并不表示相应的实参也是本地的。对于哑参能够怎样显式映射,考虑下面的可能性:

概括地讲,哑参总是映射到活动处理器集合上,虽然实参无须这样(除了在抄写性映射时)

让我们返回前面的例子:

如果COLLECT被定义为

    SUBROUTINE COLLECT( A )
    !HPF$ DISTRIBUTE A(CYCLIC)

则调用将以如下方式执行:

  1. X将从10个处理器(例如P的全部)上的BLOCK重映射到4个处理器(例如P(4:7))上的CYCLIC。这是一个多对多的变幻模式。
  2. COLLECT将在处理器P(4),P(5),P(6)和P(7)上被调用。那些处理器上的重映射数组将满足子程序中对A的访问。
  3. A将被重映射回X的分配中。这是第一步的逆过程。

注意A是分配到4个处理器上(调用中的活动处理器集合)而不是分配到全体处理器集合上。如果接口是

  SUBROUTINE COLLECT( A )
  !HPF$ DISTRIBUTE A(BLOCK)

则除了要发生一个从10个处理器上BLOCK到4个处理器上BLOCK的重映射之外,处理是相同的。即,块的大小将增加2.5倍,然后再恢复到原有的大小。另外,特别要注意A是分配到活动处理器集合上,而不是P的所有处理器上。

类似的例子:

  REAL X(100,100), Y(100,100)
  !HPF$ PROCESSORS P(4), Q(2,2)
  !HPF$ DISTRIBUTE X(BLOCK,*) ONTO P
  !HPF$ DISTRIBUTE Y(BLOCK,BLOCK) ONTO Q

  INTERFACE
  SUBROUTINE A_CAB( B )
  REAL B(:)
  !HPF$ DISTRIBUTE B *(BLOCK)
  END INTERFACE

  !HPF$ ON ( P(4:7) )
  CALL A_CAB( X( 1:100, 1 )
  !HPF$ ON HOME( X(1:100,1) )
  CALL A_CAB( X(1:100,100) )
  !HPF$ ON HOME( Y(1:100,1) )
  CALL A_CAB( Y(1:100,1) )
  !HPF$ ON HOME( Y(99,1:100) )
  CALL A_CAB( Y(99,1:100) )

可如下解释。在P(4:7)上调用A_CAB(1:100,1)将产生一个从10个处理器到4个处理器的一个重映射,就象上面的例子一样。(在这种情况下,希望编译器产生一个警告,见第四节的解释)。因为活动处理器集合没有改变,在HOME(X(1:100,1))上调用A_CAB(Y(1:100,100))不产生这种重映射(或警告);因此,描述性映射正确地指明了数据已经在正确的处理器上。最后两个例子,在它们的参数HOME上调用A_CAB(Y(1:100,1))和A_CAB(Y(99,1:100)),也是在没有重映射的情况下完成的。在这两种情况中,实参以BLOCK方式映射到处理器子集上(在第一种情况中是映射到Q的列上,第二种情况中是映射到Q的行上)。但是一些编译器不能为这些更复杂的例子产生代码。

下面的两个抄写性映射的例子也很有用:

  ! 假定
  ! PROCESSORS P(4)
  ! 是在一个模块中定义的
  REAL X(100)
  !HPF$ DISTRIBUTE X(CYCLIC(5)) ONTO P

  INTERFACE
  SUBROUTINE FOR_HELP( C )
  REAL C(:)
  !HPF$ INHERIT C
  END INTERFACE

  !HPF$ ON HOME( X(11:20) )
  CALL FOR_HELP( X(11:20) )
  !HPF$ ON ( P(1) )
  CALL FOR_HELP( X(51:60) )

第一个例子是有效的——实参被细分到活动处理器集合上。第二个例子是无效的——例如,元素X(51)存储在P(3)上,对于调用而言,该处理器不在活动处理器集合中。如果ON指令指定P(3:4)或HOME(X(11:20)),则第二个例子就将是有效的,这两种指定都映射到同一处理器集合上。

还应提及的是对外部子程序的调用。调用一个外部子程序的部分“标准”HPF2.0描述如下所示:

调用一个外部过程必须在语义上等价于调用一个普通的HPF过程。这样调用一个外部过程就好象必须发生如下行为:

这一约束被修改为

这一意图与最初的语言设计相同。存储数据的处理器不能出现后不消失;而且执行程序的处理器集合的改变必须通知程序。类似地,一些外部种类指定“所有的处理器必须同步”或“每个处理器上本地过程的执行”;这个语言被理解为:“所有活动处理器必须同步”或“每个活动处理器上本地过程的执行。”

基本原理:这就为EXTRINSIC过程和ON指令的组合提供了一个并行的fork-join模型,这一模型似乎很自然并且在语义上也很清晰。(基本原理结束)

如果过程使用交替返回,则返回目标必须与CALL语句具有相同的活动处理器集合。实际上,这就意味着作为参数传送的标号所指向的语句必须与CALL语句在同一ON块内。

基本原理:这一约束类似于禁止跳出ON块,并具有相同的理由。(基本原理结束)

ON指令中CALLS的显式使用经常与任务并行相关联。我们可以在8.4节中找到一些例子。下面的例子阐述了处理器是怎样用于一维域分解算法的:

  !HPF$ PROCESSORS PROCS(NP)
  !HPF$ DISTRIBUTE X(BLOCK) ONTO PROCS

  !在PROCS(IP)上计算ILO(IP)=下界
  !在PROCS(IP)上计算IHI(IP)=上界
  DONE = .FALSE.
  DO WHILE (.NOT. DONE)
   !HPF$ INDEPENDENT
   DO IP = 1, NP
    !HPF$ ON (PROCS(IP))
    CALL SOLVE_SUBDOMAIN( IP, X(ILO(IP):IHI(IP)) )
   END DO
   !HPF$ ON HOME(X) BEGIN
    CALL SOLVE_BOUNDARIES( X, ILO(1:NP), IHI(1:NP) )
    DONE = CONVERGENCE_TEST( X, ILO(1:NP), IHI(1:NP) )
   !HPF$ END ON
  END DO

本算法将整个计算域(数组X)分成NP个子域,并为每个处理器分配一个子域。INDEPENDENT IP循环在每个子域的内部执行一个计算。ON指令告诉编译器在执行这些(概念上的)并行操作时使用哪些处理器。特别是如果编译器不能分析SOLVE_SUBDOMAIN中的数据访问模式时,这样做可以很大程度上增加数据的局部性。子程序SOLVE_SUBDOMAIN可为它的数组参数使用一个抄写性或描述性映射,并将它放在单独的一个处理器上。在下一个阶段,处理器协作修改子域的边界并为分枝做测试。子程序SOLVE_BOUNDARIES和CONVERGENCE_TEST可用类似的RESIDENT子句具有与IP循环类似的其自己的循环。注意由于仅记录了每个子域的上下界,这就允许不同的处理器处理不同大小的子域。但是,每个子域必须“符合”X数组在一个处理器上的区域。

对实现者的建议:上面的IP循环可能是程序员所做的块结构代码中的一个普通的惯用法。一般来说,可通过将HOME子句转换为上面所做的来实现它。在这里所显示的一对一情况中(可能对于程序员很普及),可通过将处理器id号赋给循环索引变量并测试循环的范围(一次)来实现它。(对实现者的建议结束)

基本原理:一些编译器将ON信息从调用者传给被调者的行为一部分在编译时,一部分在运行时。在调用者和被调者中重复ON子句会为编译器提供更好的信息,并导致更好的生成代码。(基本原理结束)


Copyright: NPACT BACKWARDFORWARD