Sun Studio 12:Fortran 编程指南

10.1 基本概念

应用程序的并行化(或多线程) 是指对程序进行编译,使其能够在多处理器系统上或多线程环境中运行。并行化能使单个任务(如,DO 循环)运行于多个处理器(或线程)之上,从而有可能显著加快执行速度。

只有应用程序首先成为多线程程序,才能在 UltraTM 60、Sun EnterpriseTM Server 6500 或 Sun Enterprise Server 10000 等多处理器系统上高效运行。也就是说,需要对可并行执行的任务进行标识并重新编程,使其计算分布在多个处理器或线程上。

可以通过对 libthread 基元进行适当调用,来手动实现应用程序的多线程化。但可能需要进行大量的分析和重新编程工作。(有关更多信息,请参见 Solaris《 多线程编程指南》。)

Sun 编译器能自动生成在多处理器系统上运行的多线程对象代码。作为支持并行机制的主要语言元素,Fortran 编译器将关注焦点放在 DO 循环上。并行化可将循环的计算工作分配到多个处理器上,无需修改 Fortran 源程序

选择哪些循环进行并行化以及如何分配这些循环可以完全让编译器 (-autopar) 去决定,也可以由程序员使用源代码指令 (-explicitpar) 来显式指定,还可以采用组合方式 (-parallel) 来实现。


注 –

能用任何编译器并行化选项编译自行(显式)管理线程的程序。显式多线程(调用 libthread 基元)不能与用这些并行化选项编译的例程结合使用。


将程序中的所有循环都进行并行化处理并非都是有益的。只包含少量计算工作(与用于启动和同步并行任务的开销相比)的循环在并行化后,实际的运行速度可能更慢。另外,有些循环根本不能安全地进行并行化;由于语句或迭代间的依赖性,它们在并行运行时会计算出不同的结果。

隐式循环(例如,IF 循环和 Fortran 95 数组语法)和显式 DO 循环都可以由 Fortran 编译器自动进行并行化。

f95 能够检测出那些可以安全、有益地自动进行并行化的循环。但在大多数情况下,由于考虑到可能存在的隐藏副作用,这种分析肯定是保守的。(-loopinfo 选项可以显示哪些循环进行了并行化、哪些未进行并行化。)在循环前面插入源代码指令,可以显式地对分析施加影响,以控制如何并行化(或不并行化)特定的循环。但是,您随后需要负责确保循环的这种显式并行化不会导致错误的结果。

Fortran 95 编译器通过实现 OpenMP 2.0 Fortran API 指令来提供显式并行化。对于传统程序,f95 也可以接受较早的 Sun 和 Cray 风格的指令,但现在这些指令已经过时,不使用了。在 Fortran 95、C 和 C++ 中,OpenMP 已成为显式并行化的非正式标准,建议使用它来取代较早的指令风格。

有关 OpenMP 的信息,请参见《OpenMP API 用户指南》,或访问 OpenMP 网站 http://www.openmp.org

10.1.1 加速-期望目标

如果要使程序并行化以便该程序在四个处理器上运行,这样运行该程序花费的时间是否大致是在单个处理器上运行时所花费时间的四分之一(四倍加速)呢?

可能不行。可以证明(依据 Amdahl 法则):程序的总体加速性能严格受花在并行运行代码上的时间数量的限制。无论采用多少处理器都是如此。事实上,如果用 p 表示并行模式下花费的总程序执行时间的百分比,则理论加速限度为 100/(100–p);因此,如果只有 60% 的程序执行是以并行方式进行的,则最高加速倍数是 2.5,该值与处理器个数无关。对于只有四个处理器的情况,该程序的理论加速值(假设可以达到最高效率)只有 1.8 而不是 4。与总开销相比,实际加速较少。

如同优化一样,循环的选择至关重要。如果并行化的循环在总的程序执行时间中只占很小一部分,则只能获得微小的效果。要提高效率,必须并行化耗用大部分运行时间的循环。因此,第一步先要确定哪些循环是主要的,然后从此开始。

问题量在确定并行运行程序片段并进而确定加速性能中也起着重要作用。增加问题量会增加循环中完成的工作量。三重嵌套循环将会使工作量呈立方级数递增。如果并行化外层嵌套循环,则少量增加问题量便能使性能有显著提高(与未并行化性能相比)。

10.1.2 程序并行化步骤

此处列出了应用程序并行化的常规步骤:

  1. 优化。使用适当的编译器选项集,以在单个处理器上获得最佳串行性能。

  2. 配置文件。使用典型测试数据,确定程序的性能配置文件。标识最主要的循环。

  3. 基准测试。确定串行测试结果是准确的。使用这些结果以及性能配置文件作为基准。

  4. 并行化。使用选项和指令组合编译并生成并行化的可执行文件。

  5. 验证。在单个处理器和单个线程上运行并行化的程序,并检查结果,以找出可能在其中出现的不稳定性和编程错误。(将 $PARALLEL$OMP_NUM_THREADS 设置为 1;请参见10.1.5 线程数)。

  6. 测试。在几个处理器上执行各种运行以检查结果。

  7. 基准测试。在专用系统上用不同数目的处理器进行性能测量。测量性能随问题量变化的变化情况(可量测性)。

  8. 重复步骤 4 到 7。基于性能对并行化方案进行改进。

10.1.3 数据依赖性问题

不是所有的循环都可并行化。在多个处理器上以并行方式运行循环通常会导致迭代执行次序紊乱。而且,只要循环中存在数据依赖性,以并行方式执行循环的多个处理器便有可能相互干扰。

会引起数据依赖性问题的情况包括递归、约简、间接寻址以及依赖于数据的循环迭代。

10.1.3.1 依赖于数据的循环

您可以重新编写循环来消除数据依赖性,使其可以并行化。但需要进行大量的重构工作。

以下是一些通用规则:

这些是进行并行化的一般条件。在确定是否并行化循环时,编译器的自动并行化分析会考虑附加条件。但是,可以使用指令显式地强制并行化循环,甚至是那些包含抑制因素和产生错误结果的循环。

10.1.3.2 递归

在循环的某一次迭代中设置并在后续迭代中使用的变量会导致产生交叉迭代依赖性或递归。循环中的递归要求迭代以正确顺序执行。例如:


   DO I=2,N
      A(I) = A(I-1)*B(I)+C(I)
   END DO

必须在上一迭代中计算出 A(I) 的值,方能在当前迭代中(作为 A(I-1))使用。要产生正确的结果,迭代 I 必须先完成,迭代 I+1 方可执行。

10.1.3.3 约简

约简操作可将数组中的元素约简为单个值。例如,在对数组元素求和并送入单个变量时,需要在每次迭代时更新该变量:


   DO K = 1,N
     SUM = SUM + A(I)*B(I)
   END DO

如果以并行方式运行该循环的每个处理器均取得了迭代的一些子集,这些处理器将会相互干扰,从而覆盖 SUM 中的值。为使之正常工作,每个处理器每次必须执行一次求和,但顺序并不重要。

编译器会将某些常见的约简操作视为特例进行处理。

10.1.3.4 间接寻址

如果向在循环中用下标(下标值未知)标出的数组存储数据,会导致循环依赖性。例如,如果在索引数组中存在重复的值,间接寻址会依赖于顺序:


   DO L = 1,NW
     A(ID(L)) = A(L) + B(L)
   END DO

在示例中,ID 中重复的值会造成 A 中的元素被覆盖。在串行情况下,最后存储的是最终值。在并行情况下,顺序是不确定的。所使用的 A(L) 值(旧的或更新后的)依赖于顺序。

10.1.4 编译以实现并行化

Sun Studio 编译器本身支持将 OpenMP 并行化模型作为主并行化模型。有关 OpenMP 并行化的详细信息,参见《OpenMP API 用户指南》。Sun 和 Cray 风格的并行化涉及传统的应用程序,当前的 Sun Studio 编译器不再支持这些风格的并行化。

表 10–1 Fortran 95 并行化选项

选项 

标志 

自动(单独

-autopar

自动和约简 

-autopar -reduction

显示并行化哪些循环 

-loopinfo

显示显式情况下的警告 

-vpara

在栈中分配局部变量 

-stackvar

编译以实现 OpenMP 并行化 

-xopenmp

选项注释:

10.1.5 线程数

PARALLEL(或 OMP_NUM_THREADS)环境变量用来控制程序可以使用的线程的最大数量。设置该环境变量可将程序能够使用的最大线程数告之运行时系统。缺省值为 1。一般会将 PARALLELOMP_NUM_THREADS 变量设置为目标平台上可用虚拟处理器的数量。

下例展示如何设置环境变量:


demo% setenv OMP_NUM_THREADS 4       C shell


demo$ OMP_NUM_THREADS=4               Bourne/Korn shell
demo$ export OMP_NUM_THREADS

在本例中,将 PARALLEL 设置为 4,可以最多使用四个线程来执行程序。如果目标机有四个可用的处理器,这些线程将分别映射到独立的处理器。如果可用处理器数少于四个,则一些线程必须与其他线程在同一处理器上运行,这样可能会降低性能。

SunOSTM 操作系统命令 psrinfo(1M) 显示系统上可用的处理器列表:


demo% psrinfo
0      on-line   since 03/18/2007 15:51:03
1      on-line   since 03/18/2007 15:51:03
2      on-line   since 03/18/2007 15:51:03
3      on-line   since 03/18/2007 15:51:03

10.1.6 栈、栈大小和并行化

执行程序可为执行该程序的初始线程维护一个主内存栈,还可为每个辅助线程维护不同的栈。栈为临时内存地址空间,用来保存子程序调用期间的参数和 AUTOMATIC 变量。

主栈的缺省大小约为 8 兆字节。Fortran 编译器通常会将局部变量和数组作为 STATIC 进行分配(而不是在栈中)。但是,-stackvar 选项强制在栈内分配所有局部变量和数组(就像它们是 AUTOMATIC 变量一样)。建议在并行化时使用 -stackvar,因为它可增强优化程序在循环中并行化子程序调用的功能。对于包含子程序调用的显式并行化循环,-stackvar必需的。(请参见《Fortran 用户指南》中对 -stackvar 的介绍。)

使用 C shell (csh) 时,limit 命令会显示当前主栈大小,而且会对其进行设置:


demo% limit             C shell example
cputime       unlimited
filesize       unlimited
datasize       2097148 kbytes
stacksize       8192 kbytes            <- current main stack size
coredumpsize       0 kbytes
descriptors       64
memorysize       unlimited
demo% limit stacksize 65536       <- set main stack to 64Mb
demo% limit stacksize
stacksize       65536 kbytes

对于 Bourne 或 Korn shell,相应的命令为 ulimit


demo$ ulimit -a         Korn Shell example
time(seconds)        unlimited
file(blocks)         unlimited
data(kbytes)         2097148
stack(kbytes)        8192
coredump(blocks)     0
nofiles(descriptors) 64
vmemory(kbytes)      unlimited
demo$ ulimit -s 65536
demo$ ulimit -s
65536

多线程程序的每个辅助线程都有自己的线程栈。该栈模拟初始线程栈,但对于线程是唯一的。线程的 PRIVATE 数组和变量(线程的局部变量)在线程栈中分配。缺省大小在 64 位 SPARC 和 64 位 x86 平台上为 8 兆字节,在其他平台上为 4 兆字节。此大小是通过 STACKSIZE 环境变量设置的:


demo% setenv STACKSIZE 8192    <- Set thread stack size to 8 Mb    C shell
                          -or-
demo$ STACKSIZE=8192           Bourne/Korn Shell
demo$ export STACKSIZE

对于某些已并行的 Fortran 代码,可能需要将线程栈大小设置为比缺省值大的值。但是,除了反复进行错误试验,不可能知道其确切大小,特别是如果涉及到专用/局部数组就更是如此。如果栈大小太小不足以运行线程,程序将会因段故障而中止。