Sun Studio 12:C 用户指南

3.8 别名和并行化

ISO C 别名通常可防止循环并行化。存在两个对同一存储单元的可能引用时,将产生别名。请看以下示例:


示例 3–21 具有对同一存储单元的两个引用的循环


void copy(float a[], float b[], int n) {
    int i;
    for (i=0; i < n; i++) {
            a[i] = b[i]; /* S1 */
    }
}

由于变量 ab 为参数,因此 ab 可能指向重叠的内存区域;例如,如果 copy 的调用如下:


copy (x[10], x[11], 20);

在调用的例程中,copy 循环的两次连续迭代可能读/写数组 x 的同一元素。然而,如果例程 copy 的调用如下,则循环的 20 次迭代中不可能出现重叠:


copy (x[10], x[40], 20);

通常,在不知道例程如何调用的情况下,编译器不可能正确地分析此情况。编译器提供 ISO C 的关键字扩展,使您可以传达此类别名信息。有关更多信息,请参见3.8.2 限定指针

3.8.1 数组引用和指针引用

别名问题的部分原因是:C 语言可以通过指针运算来定义数组引用及定义。为使编译器有效地并行化循环(自动或显式使用 pragma),所有采用数组布局的数据均必须使用 C 数组引用语法而不是指针进行引用。如果使用指针语法,编译器将无法确定循环的不同迭代之间的关系。因此,它将保守而不会并行化循环。

3.8.2 限定指针

为使编译器有效地执行循环的并行执行任务,需要确定特定左值是否指定不同的存储区域。别名是其存储区域相同的左值。由于需要分析整个程序,因此确定对象的两个指针是否为别名是一个困难而费时的过程。例如下面的函数 vsq()


示例 3–22 带两个指针的循环


void vsq(int n, double * a, double * b) {
    int i;
    for (i=0; i<n; i++) {
            b[i] = a[i] * a[i];
    }
}

如果编译器知道指针 ab 访问不同的对象,可以并行化循环的不同迭代的执行。如果通过指针 ab 访问的对象存在重叠,编译器以并行方式执行循环将会不安全。在编译时,编译器并不能通过简单地分析函数 vsq() 来获悉 ab 访问的对象是否重叠;编辑器需要分析整个程序才能获取此信息。

限定指针用来指定哪些指定不同对象的指针,以便编译器可以执行指针别名分析。以下是函数参数声明为限定指针的函数 vsq() 示例:


void vsq(int n, double * restrict a, double * restrict b)

指针 ab 声明为限定指针,因此编译器知道 ab 指向不同的存储区域。有了此别名信息,编译器就能够并行化循环。

关键字 restrict 是一个类型限定符,与 volatile 一样,但它仅限定指针类型。使用 -xc99=all(使用 -Xs 时除外)时,restrict 识别为一个关键字。在某些情况下,您可能不希望更改源代码。可以使用以下命令行选项指定将返回赋值指针函数参数视为限定指针:


-xrestrict=[func1,…,funcn]

如果指定函数列表,则指定的函数中的指针参数将被视为限定的;否则,整个 C 文件中的所有指针参数均被视为限定的。例如,-xrestrict=vsq 限定前一个有关键字 restrict 的函数 vsq() 示例中给定的指针 ab

正确使用 restrict 至关重要。如果指针被限定为限定指针而指向不同的对象,编译器会错误地并行化循环而导致不确定的行为。例如,假定函数 vsq() 的指针 ab 指向的对象重叠,如 b[i]a[i+1] 是同一对象。如果 ab 未声明为限定指针,循环将以串行方式执行。如果 ab 被错误地限定为限定指针,编译器会并行化循环的执行,但这是不安全的,因为 b[i+1] 应在 b[i] 之后进行计算。

3.8.3 显式并行化和 Pragma

通常,编译器没有足够的信息来判断并行化的合法性或有益性。编译器支持 pragma,使程序员能够有效地并行化循环,否则编译器很难或根本无法处理这些循环。本节中其他地方详细介绍的 Sun 特定的 MP pragma 对 OpenMP 标准的支持已过时。有关该标准的指令的信息,请参见《OpenMP API 用户指南》。

3.8.3.1 串行 Pragma


注 –

Sun 特定的 MP pragma 已过时,并且不再受支持。但是,编译器改为支持 OpenMP 2.5 标准指定的 API。有关标准的指令的迁移信息,请参见《OpenMP API 用户指南》。


有两个串行 pragma,均适用于 for 循环:

#pragma MP serial_loop pragma 向编译器指示:下一个 for 循环不自动并行化。

#pragma MP serial_loop_nested pragma 向编译器指示:下一个 for 循环以及该 for 循环的作用域内嵌套的任何 for 循环均不自动并行化。

这些 pragma 的作用域以 pragma 开始,并在下列先出现的项结束:下一个块的开头、当前块内的下一个 for 循环或当前块的结尾。

3.8.3.2 并行 Pragma


注 –

Sun 特定的 MP pragma 已过时,并且不再受支持。但是,编译器改为支持 OpenMP 2.5 标准指定的 API。有关标准的指令的迁移信息,请参见《OpenMP API 用户指南》。


有一个并行 pragma:#pragma MP taskloop [options]

MP taskloop pragma 可以根据需要带下列一个或多个参数。

这些 pragma 的作用域以 pragma 开始,并在下列先出现的项结束:下一块的开始部分、当前块内部的下一个 for 循环、当前块的结尾。该 pragma 应用于 pragmas 作用域结束前的下一个 for 循环。

每个 MP taskloop pragma 只能指定一个选项;但是,pragmas 是累积的,并应用于 pragmas 作用域中的下一个 for 循环:


#pragma MP taskloop maxcpus(4)
#pragma MP taskloop shared(a,b)
#pragma MP taskloop storeback(x)

这些选项在其所应用的 for 循环之前可能会多次出现。如果存在冲突选项,编译器将发出警告消息。

for 循环的嵌套

MP taskloop pragma 应用于当前块内部的下一个 for 循环。并行化 C 不存在并行化 for 循环的嵌套。

并行化的合格性

除非另有禁止,否则 MP taskloop pragma 建议编译器应并行化指定的 for 循环。

任何具有不规则控制流和未知循环迭代增量的 for 循环均不能进行并行化。例如,包含 setjmplongjmpexitabortreturngotolabelsbreakfor 循环均不能并行化。

特别重要的是,具有迭代间依赖性的 for 循环可以进行显式并行化。这意味着,如果为此类循环指定了一个 MP taskloop pragma,除非 for 循环被取消资格,否则编译器将完全遵循该 pragma。用户有责任确保此类显式并行化不会导致错误结果。

如果为 for 循环指定了 serial_loop(或 serial_loop_nested)pragma 和 taskloop pragma,则将使用最后指定的 pragma。

请看以下示例:


#pragma MP serial_loop_nested
    for (i=0; i<100; i++) {
   # pragma MP taskloop
      for (j=0; j<1000; j++) {
      ...
 }
}

i 循环将不并行化,但是 j 循环可能并行化。

处理器数

#pragma MP taskloop maxcpus (number_of_processors) 指定要用于此循环的处理器数(如果可能)。

maxcpus 的值必须为正整数。如果 maxcpus 等于 1,指定的循环将串行执行。(请注意,将 maxcpus 设置为 1 相当于指定 serial_loop pragma。)将比较 maxcpus 的值与 PARALLEL 环境变量的解释值,使用较小的值。在未指定环境变量 PARALLEL 的情况下,该 pragma 被解释为具有值 1。

如果为一个 for 循环指定了多个 maxcpus pragma,将使用最后指定的 pragma。

变量分类

循环中使用的变量可分类为 privatesharedreductionreadonly 变量。变量只能属于其中一种分类。通过显式 pragma 只能将变量分类为 reductionreadonly 变量。请参见 #pragma MP taskloop reduction#pragma MP taskloop readonly。通过显式 pragma 或以下缺省作用域规则可将变量分类为 privateshared 变量。

privateshared 变量的缺省作用域规则

private 变量的值是处理 for 循环的某些迭代的每个处理器的私有值。换句话说,在 for 循环的一次迭代中赋给 private 变量的值不会传送给处理该 for 循环的其他迭代的其他处理器。而 shared 变量的当前值可处理 for 循环的迭代的所有处理器访问。处理循环迭代的一个处理器赋给 shared 变量的值可能会被处理循环的其他迭代的其他处理器识别。通过使用 #pragma MP taskloop 指令显式并行化并包含共享变量引用的循环,必须确保值的共享不会导致正确性问题(如竞争情况)。对显式并行化的循环中的共享变量的更新和访问,编译器不提供同步。

在分析显式并行化的循环时,编译器使用以下“缺省作用域规则”来确定变量是 private 变量还是 shared 变量:

强烈建议将显式并行化的 for 循环中使用的所有变量显式分类为 sharedprivatereductionreadonly 变量,以避免使用“缺省作用域规则”。

由于编译器对共享变量的访问不执行同步,因此在对包含数组引用等的循环使用 MP taskloop pragma 之前必须格外谨慎。如果此类显式并行化的循环中存在迭代间数据依赖性,则其并行执行会导致错误结果。编译器不一定能够检测到此类潜在问题情况并发出警告消息。无论如何,编译器不会禁止具有潜在共享变量问题的循环的显式并行化。

private 变量

#pragma MP taskloop private (list_of_private_variables)

使用此 pragma 指定所有应视为此循环私有变量的变量。此循环中使用但未显式指定为 sharedreadonlyreduction 变量的所有其他变量,按缺省作用域规则的定义,为 shared 变量或 private 变量。

private 变量的值是处理循环的某些迭代的每个处理器的私有值。换句话说,处理循环迭代的一个处理器赋给 private 变量的值不会传送给处理该循环的其他迭代的其他处理器。private 变量在循环的每次迭代开始时没有初始值,必须在循环的迭代内部第一次使用它之前,在该迭代内部设置为一个值。如果某个循环包含一个在设置之前使用其值的显式声明 private 变量,则执行包含该循环的程序将导致不确定的行为。

shared 变量

#pragma MP taskloop shared (list_of_shared_variables)

使用此 pragma 指定所有应视为此循环的 shared 变量的变量。循环中使用的但未显式指定为 privatereadonlystorebackreduction 变量的所有其他变量,按缺省作用域规则的定义,为 shared 变量或 private 变量。

shared 变量的当前值可被处理 for 循环的迭代的所有处理器访问。处理循环迭代的一个处理器赋给 shared 变量的值可能会被处理循环的其他迭代的其他处理器识别。

readonly 变量

#pragma MP taskloop readonly (list_of_readonly_variables)

readonly 变量是一类特殊的共享变量,不在循环的任何迭代中进行修改。使用此 pragma 向编译器指示,它可以对处理循环迭代的每个处理器使用该变量值的单独副本。

storeback 变量

#pragma MP taskloop storeback (list_of_storeback_variables)

使用此 pragma 指定所有应视为 storeback 变量的变量。

storeback 变量的值在循环中计算,并且计算的值在循环终止后使用。storeback 变量的最后一个循环迭代值在循环终止后使用。当变量为私有变量时,此类变量可以很好地通过此指令显式声明为 storeback 变量,通过将变量显式声明为私有变量或通过使用缺省作用域规则均可。

请注意,storeback 变量的返回存储操作发生在显式并行化循环的最后一次迭代中,而无论该迭代是否更新 storeback 变量的值。换句话说,处理循环最后一次迭代的处理器可能并不是当前包含 storeback 变量的最后更新值的同一处理器。请看以下示例:


#pragma MP taskloop private(x)
#pragma MP taskloop storeback(x)
   for (i=1; i <= n; i++) {
      if (...) {
          x=...
      }
}
   printf (“%d”, x);

在上例中,通过 printf() 调用打印出的 storeback 变量 x 的值可能与通过 i 循环的串行版本打印出的值不同,原因是,在显式并行化情况下,处理循环最后一次迭代(当 i==n 时)的处理器(即执行 x 的返回存储操作的处理器)可能不是当前包含 x 的最后更新值的同一处理器。编译器将尝试发出警告消息,提醒用户注意此类潜在问题。

在显式并行化的循环中,作为数组引用的变量不视为 storeback 变量。因此,如果需要此类返回存储操作(例如,如果作为数组引用的变量已声明为私有变量),则将作为数组引用的变量包括在 list_of_storeback_variables 中很重要。

savelast

#pragma MP taskloop savelast

使用此 pragma 指定要视为返回存储变量的所有私有变量。此 pragma 的语法如下:

#pragma MP taskloop savelast

通常,使用这种形式很方便,无需在将每个变量声明为返回存储变量时列出循环的每个私有变量。

reduction 变量

#pragma MP taskloop reduction (list_of_reduction_variables) 指定出现在 reduction 变量列表中的所有变量均将视为循环的 reduction 变量。reduction 变量的部分值可由处理循环迭代的每个处理器单独计算,其最终值可从其所有部分值中计算。有了 reduction 变量列表,便于编译器识别循环是否为约简循环,从而允许为其生成并行约简代码。请看以下示例:


#pragma MP taskloop reduction(x)
    for (i=0; i<n; i++) {
         x = x + a[i];
}

变量 x(sum) 约简变量,i 循环是 (sum) 约简循环。

调度控制

Sun ISO C 编译器支持多种 pragma,这些 pragma 可与 taskloop pragma 配合使用,以控制给定循环的循环调度策略。此 pragma 的语法是:

#pragma MP taskloop schedtype (scheduling_type)

此 pragma 可用来指定要用来调度并行化循环的特定 scheduling_typeScheduling_type 可为以下类型之一:


#pragma MP taskloop maxcpus(4)
#pragma MP taskloop schedtype(static)
    for (i=0; i<1000; i++) {
...
}

在以上示例中,四个处理器中的每个处理器将处理循环的 250 次迭代。


#pragma MP taskloop maxcpus(4)
#pragma MP taskloop schedtype(self(120))
for (i=0; i<1000; i++) {
...
}

在以上示例中,分配给每个参与处理的处理器的迭代次数按工作请求顺序依次为:

120、120、120、120、120、120、120、120、40。


#pragma MP taskloop maxcpus(4)
#pragma MP taskloop schedtype(gss(10))
for (i=0; i<1000; i++) {
...
}

在以上示例中,分配给每个参与处理的处理器的迭代次数按工作请求顺序依次为:

250、188、141、106、79、59、45、33、25、19、14、11、10、10、10。


#pragma MP taskloop maxcpus(4)
#pragma MP taskloop schedtype(factoring(10))
for (i=0; i<1000; i++) {
...
}

在以上示例中,分配给每个参与处理的处理器的迭代次数按工作请求顺序依次为:

125、125、125、125、62、62、62、62、32、32、32、32、16、16、16、16、10、10、10、10、10、10。