8.3 RESIDENT指令 |
RESIDENT子句的目的是将计算所访问的数据标志为执行处理器的本地数据。即,RESIDENT声明其作用域范围内的某种引用(或所有引用)被存储在活动处理器集合中。编译器可使用这一信息避免产生通信或简化数组地址计算。注意,一个给定的数据元素是否是本地的依赖于两个因素:
正是由于这些原因,在ON指令中增加了RESIDENT子句,该指令通常位于程序中获得所需事实的最早点。RESIDENT子句也可以作为一条独立的指令出现;这一点当对于整个ON区域来说局部性信息不为真是很有用。注意改变ON指令可能使一些RESIDENT子句无效,也可能使更多的RESIDENT子句为真。
H809 resident-clause is RESIDENT [ ( local-var-list ] )]
H810 simple-resident-directive is resident-clause
H811 block-resident-directive is resident-clause BEGIN
H812 resident-block is block-resident-directive block end-resident-directive
H813 end-resident-directive is END [ RESIDENT]
H814 local-var is variable
simple-on-directive,simple-resident-directive是executable-directive下的一个选项,而resident-block与Fortran的可执行语句相符。
RESIDENT子句中任何顶层变量必须被显式映射。类似地,RESIDENT子句在程序中的位置必须出现在具有被定义活动处理器集合的点上(例如,在一个ON块内)否则,声明语句是关于编译器怎样工作的,而不是关于程序的。
对实现者的建议:RESIDENT去掉了对于参与进/出ON子句通信的屏蔽处理器的需求。(对实现者的建议结束)
RESIDENT指令向编译器声明如果由指定的活动处理器集合执行计算,则ON中的某些数组引用被存储在局存中。如果使用simple-resident-directive形式,则声明的作用域范围是下一条Fortran语句,如果使用resident-block形式,则声明的作用域范围是代码的封闭块。如果RESIDENT以ON指令的子句出现,则ON和RESIDENT适用于相同的语句。
当在RESIDENT指令的作用域范内执行语句时遇到RESIDENT(var),则它表示词法表达式变量仅访问活动处理器集合的本地数据。(即,由ON指令最内变量所命名的处理器集合)如果通过语句访问var(例如它出现在赋值语句的右手部,或出现在一个条件表达式的估算中),则至少变量的一个拷贝和任何变量的子对象必须被映射到活动处理器上。如果通过语句对var赋值(例如,它出现在赋值语句的左手部,或READ语句的变量列表中),则变量的所有拷贝和变量的所有子对象必须安放在活动处理器集合中。将RESIDENT应用于CALL语句和函数引用了一些复杂性;这些问题将在8.3.2节中处理。
注意,RESIDENT的声明总是与周围的ON指令有关。因此如果编译器没有实现ON指令,则它在解释RESIDENT时必须非常小心。类似地,如果编译器否定了程序员指定的ALIGN和DISTRIBUTE指令,则它一般不能依赖RESIDENG子句。
最后,任意嵌套的RESIDENT指令都不考虑NEW变量,详细的见下面。
基本原理:变量读和写的不同处理是根据实现时的要求。如果读一个变量的值(但不写),则该值可从变量的任意一致性拷贝中获得。相反,重复变量的所有拷贝必须是一致的,因此RESIDENT声明如果对变量更新,则该变量的所有拷贝必须都更新。
RESIDENT声明总是与所定义的数据映射和ON子句有关,这是因为这两种信息都是确定数据引用的局部性时所必须的。数据映射决定数据存储在哪里,而ON子句决定在哪里使用它们;在本质上它们决定了一个数据路径的结束点。RESIDENT本身认为路径的长度很短;显然,如果这两个结束点不都知道,就不能衡量一条路径。(基本原理结束)
例如,考虑下面的例子:
!HPF$ ON HOME(Z(I)), RESIDENT(X,Y,RECORD(I))
X(I) = Y(I+1) + RECORD(I)%FIELD1 + RECORD(I+1)%FIELD2
指令声明了下列事实:
如果没有local-var-list,则除了在周围的ON指令中被定义为NEW的那些变量,RESIDENT体的执行过程中所引用的所有变量在上面所描述的意义下,都是本地的。即,对于任意变量值的每一个使用,变量的至少一个拷贝将被映射到ON处理器集合中。同样,对于为该变量赋值的每一个操作,该变量的所有拷贝都要映射到ON处理器集合上。NEW变量的引用和赋值总是被认为是本地的。如果不存在函数或子程序引用,则在指令作用域范围内列举所有的变量引用只是一个语法上的装饰而已。(见8.3.2节中关于RESIDNET子句应用于子程序调用的讨论。)应该再命名一个ALL_RESIDENT子句;但是现有的形式仍然没有向指令的子语言中增加其它的关键字。
注意如果活动集合包括多于一个处理器,则RESIDENT仅声明变量是存储在这些处理器的某一个中。例如,如果一条语句在处理器排列的一个区域上执行,则RESIDENT子句中的一些变量可能需要在该区域内进行通信。但是那些变量不需在该区域之外的处理器上进行通信。
基本原理:这种解释的另一种说法是RESIDENT子句中所命名的任意变量对于所有处理器都是本地的,例如通过复制。虽然当然允许进一步的优化,但那不是一般的情形。另外,似乎不能俘获应用于CALL语句或复合语句的ON指令的含义。例如,
!HPF$ PROCESSORS PROCS(MP,MP)
!HPF$ DISTRIBUTE X(BLOCK,BLOCK) {\tt ONTO} PROCS
!HPF$ ON HOME(PROCS(1,1:MP)), RESIDENT( X(K,1:N) )
CALL FOO( X(K,1:N) )
预计是在处理器排列的一行上调用FOO,并将元素X传送到相应位置。这是当前的定义所做的;如果RESIDENT表示“安放在每个处理器上”,则调用将强制复制X。(基本原理结束)
RESIDENT指令类似于INDEPENDENT指令,这是因为如果它是正确的,它不改变程序的含义。如果RESIDENT不正确,则该程序不符合标准(这样就是未定义的)象INDEPENDENT指令一样,编译器可以使用RESIDENT子句中的信息,如果它不满足编译器的目的,则编译器也可以忽略它。如果编译器能够检测出RESIDENT子句是错误的(例如一个RESIDENT变量在定义上是非本地的),则它可以产生一个警告。但是与INDEPENDENT指令不同的是,RESIDENT子句的真实性依赖于计算映射(用ON子句指定)和数据映射(用DISTRIBUTE和ALIGN子句指定);如果编译器放弃了这些中的任意一个,则它就不能使用RESIDENT指令中的信息。
基本原理:对于优化器来说,知道一个引用是本地的是一个很有价值的信息。它与HPF中用短语来声明这一事实思想相一致,编译器可使用它作各种优化。将它解释为对编译器的建议似乎有一些不足。这一建议可被说明成的一些可能方式和反面论点是:
象8.2.3节一样,在这里我们的目标是提示一些习惯用法,这些习惯用法一般来说对程序员很有用。我们以扩展前面的两个例子开始。
在编译器不能检测访问模式的情况下,RESIDENT最有用。这一点经常是由于间接使用引起的,见如下的例子:
REAL X(N), Y(N)
INTEGER IX1(M), IX2(M)
!HPF$ PROCESSORS P(NP)
!HPF$ DISTRIBUTE (BLOCK) ONTO P :: X, Y
!HPF$ DISTRIBUTE (BLOCK) ONTO P :: IX, IY
!HPF$ INDEPENDENT
DO I = 1, N
!HPF$ ON HOME( X(I) ), RESIDENT( Y(IX(I)) )
X(I) = Y(IX(I)) - Y(IY(I))
END DO
!HPF$ INDEPENDENT
DO J = 1, N
!HPF$ ON HOME( IX(J) ), RESIDENT( Y )
X(J) = Y(IX(J)) - Y(IY(J))
END DO
!HPF$ INDEPENDENT
DO K = 1, N
!HPF$ ON HOME( X(IX(K)) ), RESIDENT( X(K) )
X(K) = Y(IX(K)) - Y(IY(K))
END DO
就象我们在8.2.3节所看到的,在I循环中,X(I)总是本地的而IX(I)和IY(I)则很少是。上面的RESIDENT保证了Y(IX(I))也是本地的。这最可能是根据产生IX的算法的一些特性(例如,如果对于所有的I,IX(I)=I)。注意对于一个表达式(例如Y(IX(I)))来说,即使它的某一个子表达式(IX(I))不是本地的,它也可能是本地的。
指令没有给出关于Y(IY(I))的信息;它可能仅有一个非本地值,或者它的所有值都可能是非本地的。(我们假定如果没有非本地值,则RESIDENT子句也将包括Y(IY(I))。)如果存在许多由这个表达式所引用的本地元素,且它们可以很容易地从本地元素中分离出来,则很值得重构该循环以便使其对编译器更清晰。例如假定我们知道在每个处理器上仅有X的第一个和最后一个元素是非本地的。则可将此循环分割成这样:
!HPF$ INDEPENDENT, NEW(LOCALI)
DO I = 1, N
!HPF$ ON HOME( X(I) ), RESIDENT( Y(IX(I)), Y(IY(I)) ) BEGIN
LOCALI = MOD(I,N/NP)
IF (LOCALI\=1 .AND. LOCALI\=0) THEN
X(I) = Y(IX(I)) - Y(IY(I))
END IF
!HPF$ END ON
END DO
!HPF$ INDEPENDENT, NEW(LOCALI)
DO I = 1, NP
!HPF$ ON (P(I)), RESIDENT( X(LOCALI), Y(IX(LOCALI)) ) BEGIN
LOCALI = (I-1)*N/NP
X(LOCALI) = Y(IX(LOCALI)) - Y(IY(LOCALI))
LOCALI = I*N/NP
X(LOCALI) = Y(IX(LOCALI)) - Y(IY(LOCALI))
!HPF$ END ON
END DO
第一个循环(低效地)处理Y(IY(I))的本地元素,而第二个(更高效地)处理剩下的。在大多数机器上,将这两个循环都重写以避免除操作是有利的,例如通过创建一个逻辑屏蔽priori。
在J循环中,RESIDENT子句声明Y的所有被访问元素都是本地的。在这种情况中,它等价于声明
!HPF$ RESIDENT(Y(IX(J)),Y(IY(J)))
虽然原始的RESIDENT子句仅引用了词法表达式Y,编译器能够推断出子表达式也是本地的。这是因为一个子对象与其父对象处于不同的处理器是不可能的。这一观察结果通常可以大量缩短RESIDENT子句。
在K循环中,下列引用是本地的:
注意即使一个引用没有显式出现在RESIDENT子句中,它也可能是本地的。好编译器的一个标志是它可以集中指示出这些元素。
由于一个RESIDENT子句是一个动作的声明,因此编译器可从其中获得许多推断。例如,考虑下列情况:
!HPF$ ALIGN Y(I) WITH X(I)
!HPF$ ALIGN Z(J) WITH X(J+1)
!HPF$ ON HOME( X(K) ), RESIDENT( X(INDX(K)) )
X(K) = X(INDX(K)) + Y(INDX(K)) + Z(INDX(K))
在编译赋值语句时,编译器可以建立下列假设(假定它同时照顾到了ALIGN指令和ON指令):
编译器从上面的代码中不能作出任何关于INDX(K)和Z(INDX(K))的假设。没有指示INDX是怎样相对于X进行映射的,因此ON指令未给出指导。注意表达式(在这里,X(INDX(K)))是本地的并不表示它的子表达式(在这里,INDX(K))也是本地的。类似的,如果Z(INDX(K))是本地的,则Z的映射不确定;它指出Z(INDX(K)-1)是本地的,但没有太大的用处。如果编译器具有额外的信息(例如,X以BLOCK分配且INDX(K)没有接近块边界),则它有可能作出额外的推理。
对实现者的建议:好编译器的一个标志是它积极地传播RESIDENT声明。这可能会大大缩减通信开销。注意下面的“对用户的建议”中的情况。(对实现者的建议结束)
对用户的建议:各种编译器的不同之处在于它们进行这些推导时的积极性不同。高质量的编译器能够将更多的引用标志为本地的,且使用这一信息来去除数据移动。所有的编译器都应承认如果数组的一个元素是本地的,则具有同样静态映射(例如一起对准的数组,或者具有相同DISTRIBUTE模式和相同大小的数组)的任意其它数组的相同元素也将是本地的。即,任意编译器都应该将上面例子中的Y(INDX(K))看作是本地的。动态改变数组映射(例如REALIGN和REDISTRIBUTE )将会限制这种信息和信息的传播。另外,可能改变子表达式的赋值(例如,对K或对上面例子中INDX任意元素的赋值)会强制编译器保守地进行推导。(对用户的建议结束)
如果RESIDENT指令应用于CALL语句或函数调用中,则声明会更巧妙。
基本原理:传播关于词法特性行为的声明很难一致和有用地定义。例如,考虑上面的代码段所调用的下列函数:
REAL FUNCTION F( X, Y )
REAL X, Y(:), B(I)
!HPF$ ENVIRONMENT :: ON HOME(X)
!HPF$ INHERIT Y
!HPF$ ALIGN B(:) WITH Y(:)
INTEGER I
USE MODULE_DEFINING_A
Z = 0.0
DO I = 1, SIZE(Y)
Z = Z + A(I)*X + B(I)*Y(I)
END DO
F = Z
END
假定A被定义为模块MODULE_DEFINING_A中的一个分布的全局数组。关于F中的操作,RESIDENT应表示什么?RESIDENT指令中的表达式A(I)理所当然仅表示对调用者中可见的数组A的引用,或者它也可以表示对任意名字为A的数组的引用。注意调用者中的A可以是本地的,F中的全局数组可以与A相同(如果调用者使用MODULE_DEFINING_A),也可以是一个不同的全局数组(如果调用者使用了一个不同的模块)。一个可能的受限制的情况是数组B。函数F中的数组B是本地的,这样就不同于调用者;但是,由于对ON指令的约束,本地B必须被映射到ON处理器集合上。这样,RESIDENT声明为真就是无关紧要的。为进一步将问题弄乱,RESIDENT变量似乎应用于同那些变量相关联的哑参。不幸的是,这意味着调用者中的词法表达式B指向F中的词法表达式Y,它将“词法”的定义扩展出了断点之外。正是由于这些原因,决定将RESIDENT子句中命名变量的含义限制为指令的词法范围之内。(基本原理结束)
基本原理:如果数据对于被调过程是本地的,则RESIDENT声明总是为真。这一点之所以为真是因为被调过程必须使用一个说明性的ON子句,该子句对可以存储任何本地显式映射变量的处理器集合进行了限制。上面的定义将这一声明扩展到所有的全局显式映射数据,生成了一个非常强大的指令。该指令类似于INDEPENDENT的含义,因为它也是对循环中的任意被调过程中的变量访问进行声明。RESIDENT的另一个语义是它避免了传播过程间声明。(例如它将有变量列表描述和无变量列表描述同样对待)但是在某种机器上这还不足以提供足够的信息来优化代码。特别是,它使得消息传递机器上的任务并行变得很难。(基本原理结束)
对实现者的建议:没有变量列表的RESIDENT保证了被调过程不会在ON处理器集合之外产生单边通信。除非运行时系统有额外的约束,否则这种过程只能在“活动”处理器上被调用。(例如,如果运行时系统要求所有的处理器参加集合通信)。
RESIDENT的其它形式提供了可在过程间传播的信息。例如,如果将子程序的实参声明成RESIDENT且以抄写方式传送,则被调过程中与其对准的任何变量也是RESIDENT。如果没有传播信息,则唯一的结果是缺少优化。(对实现者的建议结束)
对用户的建议:虽然RESIDENT声明可应用于过程间,但绝不保证所有的编译器都将利用这一信息。特别是分离编译限制了这种传播的发生。因此一个好的经验是在调用者的ON指令和被调过程中都包括RESIDENT 。(在这里,当然假定这一声明是真的)这就保证了编译器在编译过程调用时能够获得RESIDENT信息。这一点对于没有变量列表的RESIDENT子句尤其有用;知道所有的被访问数据是本地的可以允许进行许多本不可能发生的优化。(对用户的建议结束)
局部性信息在过程间是很重要的。在这里,使用没有local-var-list的RESIDENT指令有很多好处。考虑下面的对于8.2.4节中的块结构化例子进行的扩展:
!HPF$ PROCESSORS PROCS(NP)
!HPF$ DISTRIBUTE X(BLOCK) ONTO PROCS
! 在PROCS(IP)上计算ILO(IP) = lower bound
! 在PROCS(IP)上计算IHI(IP) = upper bound
DONE = .FALSE.
DO WHILE (.NOT. DONE)
!HPF$ INDEPENDENT
DO IP = 1, NP
!HPF$ ON (PROCS(IP)), RESIDENT
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) )
!HPF$ RESIDENT
DONE = CONVERGENCE_TEST( X, ILO(1:NP), IHI(1:NP) )
!HPF$ END ON
END DO
我们知道INDEPENDENT IP循环是在每个子域的内部执行一个计算,其中每个子域被映射到一个专门的处理器上。第一个RESIDENT子句另外告诉编译器任何一个子域都不使用其它处理器上的数据。没有这一信息,编译器就不得不假定一个最坏的方案,在该方案中每个子域更新的执行都是基于非本地只读数据。如果不违反INDEPENDENT指令,其它处理器就不能写任何非本地数据;但是如果不对数据进行更新(例如一个大的浏览表),就可将这些数据存储在远地。特别是在非共享存储的机器上,访问这一远程数据将会很难。RESIDENT子句保证了无须考虑这种可能性。SOLVE_SUBDOMAIN所要求的所有本地数据都存储在本地。第二个RESIDENT子句声明用于CONVERGENCE_TEST的所有数据都存储在与X相同的处理器上。对于SOLVE_BOUNDARIES就不能这么说,这是因为它不在RESIDENT指令的作用域范围之内。例如,一些必要的数据可能是在一个非PROCS的处理器排列上。访问这些数据可能会象上面所描述的那样产生一个计算上的瓶颈。
另外,要注意提供编译器信息的RESIDENT子句的使用。很少有编译器能够解释对ILO和IHI的赋值,而且甚至没有一个编译器试图理解上面代码段的注释。
Copyright: NPACT |