本章概述多处理器并行化并介绍 Solaris SPARC 和 x86 多处理器平台上 Fortran 95 的功能。
另请参见 Rajat Garg 和 Ilya Sharapov 合著的《Techniques for Optimizing Applications: High Performance Computing》,Sun Microsystems BluePrints 出版 (http://www.sun.com/blueprints/pubs.html)。
应用程序的并行化(或多线程) 是指对程序进行编译,使其能够在多处理器系统上或多线程环境中运行。并行化能使单个任务(如,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。
如果要使程序并行化以便该程序在四个处理器上运行,这样运行该程序花费的时间是否大致是在单个处理器上运行时所花费时间的四分之一(四倍加速)呢?
可能不行。可以证明(依据 Amdahl 法则):程序的总体加速性能严格受花在并行运行代码上的时间数量的限制。无论采用多少处理器都是如此。事实上,如果用 p 表示并行模式下花费的总程序执行时间的百分比,则理论加速限度为 100/(100–p);因此,如果只有 60% 的程序执行是以并行方式进行的,则最高加速倍数是 2.5,该值与处理器个数无关。对于只有四个处理器的情况,该程序的理论加速值(假设可以达到最高效率)只有 1.8 而不是 4。与总开销相比,实际加速较少。
如同优化一样,循环的选择至关重要。如果并行化的循环在总的程序执行时间中只占很小一部分,则只能获得微小的效果。要提高效率,必须并行化耗用大部分运行时间的循环。因此,第一步先要确定哪些循环是主要的,然后从此开始。
问题量在确定并行运行程序片段并进而确定加速性能中也起着重要作用。增加问题量会增加循环中完成的工作量。三重嵌套循环将会使工作量呈立方级数递增。如果并行化外层嵌套循环,则少量增加问题量便能使性能有显著提高(与未并行化性能相比)。
优化。使用适当的编译器选项集,以在单个处理器上获得最佳串行性能。
配置文件。使用典型测试数据,确定程序的性能配置文件。标识最主要的循环。
基准测试。确定串行测试结果是准确的。使用这些结果以及性能配置文件作为基准。
并行化。使用选项和指令组合编译并生成并行化的可执行文件。
验证。在单个处理器和单个线程上运行并行化的程序,并检查结果,以找出可能在其中出现的不稳定性和编程错误。(将 $PARALLEL 或 $OMP_NUM_THREADS 设置为 1;请参见10.1.5 线程数)。
测试。在几个处理器上执行各种运行以检查结果。
基准测试。在专用系统上用不同数目的处理器进行性能测量。测量性能随问题量变化的变化情况(可量测性)。
重复步骤 4 到 7。基于性能对并行化方案进行改进。
不是所有的循环都可并行化。在多个处理器上以并行方式运行循环通常会导致迭代执行次序紊乱。而且,只要循环中存在数据依赖性,以并行方式执行循环的多个处理器便有可能相互干扰。
会引起数据依赖性问题的情况包括递归、约简、间接寻址以及依赖于数据的循环迭代。
您可以重新编写循环来消除数据依赖性,使其可以并行化。但需要进行大量的重构工作。
以下是一些通用规则:
仅当所有迭代均写至截然不同的内存位置时,循环才是与数据无关的。
迭代可以从相同位置读取,只要无任何迭代写至这些位置。
这些是进行并行化的一般条件。在确定是否并行化循环时,编译器的自动并行化分析会考虑附加条件。但是,可以使用指令显式地强制并行化循环,甚至是那些包含抑制因素和产生错误结果的循环。
在循环的某一次迭代中设置并在后续迭代中使用的变量会导致产生交叉迭代依赖性或递归。循环中的递归要求迭代以正确顺序执行。例如:
DO I=2,N A(I) = A(I-1)*B(I)+C(I) END DO |
必须在上一迭代中计算出 A(I) 的值,方能在当前迭代中(作为 A(I-1))使用。要产生正确的结果,迭代 I 必须先完成,迭代 I+1 方可执行。
约简操作可将数组中的元素约简为单个值。例如,在对数组元素求和并送入单个变量时,需要在每次迭代时更新该变量:
DO K = 1,N SUM = SUM + A(I)*B(I) END DO |
如果以并行方式运行该循环的每个处理器均取得了迭代的一些子集,这些处理器将会相互干扰,从而覆盖 SUM 中的值。为使之正常工作,每个处理器每次必须执行一次求和,但顺序并不重要。
编译器会将某些常见的约简操作视为特例进行处理。
如果向在循环中用下标(下标值未知)标出的数组存储数据,会导致循环依赖性。例如,如果在索引数组中存在重复的值,间接寻址会依赖于顺序:
DO L = 1,NW A(ID(L)) = A(L) + B(L) END DO |
在示例中,ID 中重复的值会造成 A 中的元素被覆盖。在串行情况下,最后存储的是最终值。在并行情况下,顺序是不确定的。所使用的 A(L) 值(旧的或更新后的)依赖于顺序。
Sun Studio 编译器本身支持将 OpenMP 并行化模型作为主并行化模型。有关 OpenMP 并行化的详细信息,参见《OpenMP API 用户指南》。Sun 和 Cray 风格的并行化涉及传统的应用程序,当前的 Sun Studio 编译器不再支持这些风格的并行化。
。
表 10–1 Fortran 95 并行化选项
选项 |
标志 |
---|---|
自动(单独) |
-autopar |
自动和约简 |
-autopar -reduction |
显示并行化哪些循环 |
-loopinfo |
显示显式情况下的警告 |
-vpara |
在栈中分配局部变量 |
-stackvar |
编译以实现 OpenMP 并行化 |
-xopenmp |
选项注释:
大多数选项具有等效的同义字,例如 -autopar 和 -xautopar。可以使用其一。
不应将编译器 prof/gprof 文件配置选项 -p、-xpg 和 -pg 与任何并行化选项一起使用。这些文件配置选项的运行时支持并非线程安全。运行时可能产生无效结果或段故障。
-reduction 需要使用 -autopar。
-autopar 包括 -depend 和循环结构优化。
-noautopar、-noreduction 是否定选项。
并行化选项的顺序可以任意,但必须均为小写形式。
约简操作不在显式并行循环中进行分析。
-xopenmp 还会自动调用 -stackvar。
选项 -loopinfo 和 -vpara 必须与并行化选项之一结合使用。
PARALLEL(或 OMP_NUM_THREADS)环境变量用来控制程序可以使用的线程的最大数量。设置该环境变量可将程序能够使用的最大线程数告之运行时系统。缺省值为 1。一般会将 PARALLEL 或 OMP_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 |
执行程序可为执行该程序的初始线程维护一个主内存栈,还可为每个辅助线程维护不同的栈。栈为临时内存地址空间,用来保存子程序调用期间的参数和 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 代码,可能需要将线程栈大小设置为比缺省值大的值。但是,除了反复进行错误试验,不可能知道其确切大小,特别是如果涉及到专用/局部数组就更是如此。如果栈大小太小不足以运行线程,程序将会因段故障而中止。
使用 -autopar 选项,f95 编译器会自动查找可以有效并行化的 DO 循环。然后对这些循环进行转换,将其迭代均匀分布在可用的处理器上。编译器会生成实现这一目标所需的线程调用。
编译器的依赖性分析会将 DO 循环转换成可并行化的任务。编译器可能会重构循环,分离出将要串行运行的不可并行化部分。然后将工作均匀分布在可用的处理器上。每个处理器执行不同的迭代块。
例如,对于四个 CPU 和具有 1000 次迭代的并行化循环,每个线程将执行含有 250 次迭代的程序块。
处理器 1 执行迭代 |
1 |
至 |
250 |
处理器 2 执行迭代 |
251 |
至 |
500 |
处理器 3 执行迭代 |
501 |
至 |
750 |
处理器 4 执行迭代 |
751 |
至 |
1000 |
只有不依赖于计算执行顺序的循环才能成功进行并行化。编译器的依赖性分析拒绝对那些具有内在数据依赖性的循环进行并行化。如果不能完全确定循环中的数据流,编译器会保守行事,不进行并行化。另外,如果能确定性能增益抵不上总开销,编译器也不会对循环进行并行化。
注意:编译器总是选择对使用静态循环调度的循环进行并行化-即将循环中的工作拆分到多个等效的迭代块中。其他调度方案可以用本章后面所述的显式并行化指令来指定。
示例:数组/标量:
dimension a(10) real m(100,10), s, u, x, z equivalence ( u, z ) pointer ( px, x ) s = 0.0 ... |
m 和 a 都是数组变量;s 是纯标量。变量 u、x、z 和 px 是标量变量,但不是纯标量。
不具有任何交叉迭代数据依赖性的 DO 循环由 -autopar 自动并行化。自动并行化的一般标准是:
只有显式 DO 循环和隐式循环(如 IF 循环和 Fortran 95 数组语法)才可以并行化。
循环内每个迭代的数组变量值不能依赖于循环内任何其他迭代的数组变量值。
循环中的计算不能有条件地改变循环终止后引用的任何纯标量变量。
循环中的计算不能在各次迭代间改变标量变量。这称为循环携带依赖性。
循环体内的工作量必须要超过并行化开销。
编译器可以自动消除显示的引用以在循环中创建数据依赖性。这种转换有许多,其中之一会利用某些数组的专用版本。通常,如果编译器能确定这种数组在原始循环中只是作为临时存储使用,它便会这样做。
示例:使用 -autopar,通过专用数组消除了依赖性:
parameter (n=1000) real a(n), b(n), c(n,n) do i = 1, 1000 <--Parallelized do k = 1, n a(k) = b(k) + 2.0 end do do j = 1, n-1 c(i,j) = a(j+1) + 2.3 end do end do end |
在此例中,外层循环被并行化,并在独立的处理器上运行。虽然内层循环对数组 a 的引用看起来会导致数据依赖性,但编译器会生成数组的临时专用副本,使外层循环迭代变得独立。
在自动并行化过程中,如果以下条件下成立,编译器不会对循环进行并行化:
DO 循环嵌套在已并行化的另一 DO 循环内。
流控制允许跳出 DO 循环。
用户级子程序在循环内被调用
循环中有 I/O 语句
循环内的计算会改变具有别名的标量变量
在多线程多处理器环境中,对循环嵌套中最外层循环(而不是最内层循环)进行并行化最有效。由于并行处理通常涉及相对较大的循环开销,所以并行化最外层循环会最大程度地减少开销并增加每个线程完成的工作量。在自动并行化情况下,编译器从最外层嵌套循环开始进行循环分析,然后继续向内进行直至找到可并行化的循环。一旦嵌套中的某个循环被并行化,便会略过该并行循环内所包含的循环。
将一个数组转换为一个标量的计算称为约简操作。典型的约简操作有矢量元素的求和或求积。违反循环中计算标准的约简操作不能在各次迭代间以累积方式改变标量变量。
示例:向量元素的约简求和:
s = 0.0 do i = 1, 1000 s = s + v(i) end do t(k) = s |
但是,对于某些操作,如果约简是阻止并行化的唯一因素,仍然可以对循环进行并行化。常见约简操作出现频率很高,因而编译器能够将其视为特例进行并行化。
自动并行化分析不包括约简操作的识别,除非随 -autopar 或 -parallel 一同指定了 -reduction 编译器选项。
如果某一可并行化的循环包含表 10–2 中列出的某一项约简操作,则当指定了 -reduction 时,编译器将会对其进行并行化。
下表列出了编译器识别的约简操作。
表 10–2 识别的约简操作
数学运算 |
Fortran 语句模板 |
---|---|
求和 |
s = s + v(i) |
求积 |
s = s * v(i) |
点积 |
s = s + v(i) * u(i) |
最小值 |
s = amin( s, v(i)) |
最大值 |
s = amax( s, v(i)) |
OR |
do i = 1, n b = b .or. v(i) end do |
AND |
b = .true. do i = 1, n b = b .and. v(i) end do |
非零元素计数 |
k = 0 do i = 1, n if(v(i).ne.0) k = k + 1 end do |
识别所有形式的 MIN 和 MAX 函数。
计算并行执行的顺序与在单个处理器上串行执行的顺序不同。
计算顺序会影响浮点型数的求和或求积。硬件浮点加法和乘法不是结合式的。可能会出现舍入、溢出或下溢误差,具体取决于操作数关联的方式。例如,(X*Y)*Z 和 X*(Y*Z) 可能会得出不同的有效数。
在一些情况下,该误差是不能接受的。
示例:舍入,求介于 –1 和 +1 之间的 100,000 个随机数之和:
demo% cat t4.f parameter ( n = 100000 ) double precision d_lcrans, lb / -1.0 /, s, ub / +1.0 /, v(n) s = d_lcrans ( v, n, lb, ub ) ! Get n random nos. between -1 and +1 s = 0.0 do i = 1, n s = s + v(i) end do write(*, ’(" s = ", e21.15)’) s end demo% f95 -O4 -autopar -reduction t4.f |
结果会随着处理器的个数而变化。下表展示了介于 – 1 和 +1 之间的 100,000 个随机数之和。
处理器数 |
输出 |
---|---|
1 |
s = 0.568582080884714E+02 |
2 |
s = 0.568582080884722E+02 |
3 |
s = 0.568582080884721E+02 |
4 |
s = 0.568582080884724E+02 |
在这种情况下,对于随机开始的数据而言,10-14 阶的舍入误差是可以接受的。有关更多信息,请参见 Sun《数值计算指南》。
本部分介绍 f95 识别的源代码指令,这些指令用来显式指示要并行化的循环以及要使用的策略。
Fortran 95 编译器现在完全支持 OpenMP Fortran API 作为主并行化模型。有关其他信息,请参见《OpenMP API 用户指南》。
SPARC 平台上的 Sun Studio 编译器不再支持传统的 Sun 风格和 Cray 风格的并行化指令,x86 平台上的编译器不接受这些指令。
程序的显式并行化需要预先分析并深入理解应用程序代码以及共享内存并行化概念。
在 DO 循环之前紧接着放置的指令标记这些循环将要进行并行化。用 -xopenmp 进行编译,可以识别 OpenMP Fortran 95 指令并生成并行化的 DO 循环代码。并行化指令是用来指示编译器并行化(或不并行化)指令后面的 DO 循环的注释行。指令又称编译指示。
在选择要标记进行并行化的循环时,要小心行事。即使存在并行运行时会导致循环计算结果错误的数据依赖性,编译器也会为所有标有并行化指令的循环生成线程化的并行代码。
如果用 libthread 基元编写自己的多线程代码,请勿使用任何编译器并行化选项-编译器不能并行化已使用线程库用户调用并行化的代码。
循环是 DO 循环,而不是 DO WHILE 循环或 Fortran 95 数组语法。
循环内每个迭代的数组变量值不依赖于循环内任何其他迭代的数组变量值。
如果循环会改变某一标量变量,该变量值在循环终止后不会被使用。此类标量变量在循环终止后不能保证具有定义的值,因为编译器不会自动确保其正确回存。
对于每次迭代,循环内调用的任何子程序都不引用或改变其他任何迭代的数组变量值。
DO 循环索引必须是整数。
专用变量或数组归循环的单次迭代专用。在一次迭代中赋予专用变量或数组的值不会传播给循环内的其他任何迭代。
共享变量或数组在所有迭代间共享。在某次迭代中赋予共享变量或数组的值为循环内的其他迭代所见。
如果显式并行化的循环包含共享引用,则必须确保共享不会造成正确性问题。编译器在共享变量的更新或访问上不同步。
如果在一个循环内将某变量指定为专用,并且其唯一一次初始化位于另一循环中,则该变量的值在此循环中可能保持未定义。
循环(或从已调用例程内调用的任何子程序)中的子程序调用可能会导致产生数据依赖性,这种数据依赖性很容易被忽略,而不通过调用链深入分析数据和控制流。尽管最好是并行化执行大量工作的最外层循环,但这些循环往往正是涉及子程序调用的循环。
由于这种过程间的分析很困难并且会大大增加编译时间,所以自动并行化模式不会尝试这样做。对于显式并行化,编译器会为标有 PARALLEL DO 或 DOALL 指令的循环生成并行化代码,即使它包含子程序调用。确保此循环以及此循环包括的所有循环(包括被调用的子程序)中不存在任何数据依赖性仍是程序员的责任。
不同线程多次调用某个例程会造成问题,这些问题源自对互相干扰的局部静态变量的引用。使例程中的所有局部变量均为自动而不是静态可防止这种情况。此时,子程序的每次调用都会在栈中保留自己唯一的局部变量存储,任何两次调用均不会相互干扰。
在 AUTOMATIC 语句中列出子程序局部变量或用 -stackvar 选项编译子程序,通过这两种方法中的任一种,可以使子程序局部变量变为驻留在栈中的自动变量。但是,DATA 语句中初始化的局部变量必须进行改写,才能在实际赋值中进行初始化。
将局部变量分配给栈会造成栈溢出。有关如何增加栈大小的信息,请参见10.1.6 栈、栈大小和并行化。
一般而言,如果您显式指导编译器对循环进行并行化,编译器就会执行。但也有例外情况-存在一些编译器不进行并行化的循环。
下面是可检测到的主要抑制因素,这些抑制因素可以防止对 DO 循环进行显式并行化:
DO 循环嵌套在已并行化的另一 DO 循环内。
该例外情况也适用于间接嵌套。如果显式并行化包含子例程调用的循环,那么,即使要求编译器并行化该子例程中的循环,这些循环在运行时也不会以并行方式运行。
流控制语句允许跳出 DO 循环。
循环的索引变量受副作用影响,例如被等价。
通过使用 -vpara 和 -loopinfo 进行编译,可以得到诊断消息,指出在显式并行化循环过程中编译器是否检测到问题。
下表列出了编译器检测到的典型并行化问题:
表 10–3 显式并行化问题
问题 |
已并行化 |
警告消息 |
---|---|---|
循环嵌套在并行化了的另一循环内。 |
否 |
否 |
循环在并行化循环体内调用的某个子例程中。 |
否 |
否 |
流控制语句允许跳出循环。 |
否 |
是 |
循环的索引变量受副作用影响。 |
是 |
否 |
循环中的某变量具有循环携带依赖性。 |
是 |
是 |
在循环中使用 I/O 语句-通常是不明智的,因为输出顺序无法预料。 |
是 |
否 |
示例:嵌套循环:
... !$OMP PARALLEL DO do 900 i = 1, 1000 ! Parallelized (outer loop) do 200 j = 1, 1000 ! Not parallelized, no warning ... 200 continue 900 continue ... |
示例:子例程中已并行化的循环:
program main ... !$OMP PARALLEL DO do 100 i = 1, 200 <-parallelized ... call calc (a, x) ... 100 continue ... subroutine calc ( b, y ) ... !$OMP PARALLEL DO do 1 m = 1, 1000 <-not parallelized ... 1 continue return end |
在此例中,由于子例程本身是以并行方式运行的,所以子例程中的循环未被并行化。
示例:跳出循环:
!$omp parallel do do i = 1, 1000 ! <- Not parallelized, error issued ... if (a(i) .gt. min_threshold ) go to 20 ... end do 20 continue ... |
如果标记进行并行化的循环外有转跳,编译器会发出诊断错误。
示例:循环中的某个变量具有循环携带依赖性:
demo% cat vpfn.f real function fn (n,x,y,z) real y(*),x(*),z(*) s = 0.0 !$omp parallel do private(i,s) shared(x,y,z) do i = 1, n x(i) = s s = y(i)*z(i) enddo fn=x(10) return end demo% f95 -c -vpara -loopinfo -openmp -O4 vpfn.f "vpfn.f", line 5: Warning: the loop may have parallelization inhibiting reference "vpfn.f", line 5: PARALLELIZED, user pragma used |
在此,循环被并行化,但在警告中诊断出可能的循环携带依赖性。但要注意,编译器并不能诊断出所有循环依赖性。
在下列情况下,可以在并行执行的循环中执行 I/O:
来自不同线程的输出相互交错(程序输出是非确定的),这一点并不重要。
可以确保并行执行循环的安全性。
示例:循环中有 I/O 语句
!$OMP PARALLEL DO PRIVATE(k) do i = 1, 10 ! Parallelized k = i call show ( k ) end do end subroutine show( j ) write(6,1) j 1 format(’Line number ’, i3, ’.’) end demo% f95 -openmp t13.f demo% setenv PARALLEL 4 demo% a.out |
Line number 9. Line number 4. Line number 5. Line number 6. Line number 1. Line number 2. Line number 3. Line number 7. Line number 8. |
但递归的 I/O,即 I/O 语句包含对本身执行 I/O 的函数的调用,将会造成运行时错误。
OpenMP 是用于多处理器平台的并行编程模型,即将成为 Fortran 95、C 和 C++ 应用程序的标准编程实践方案。它是 Sun Studio 编译器的首选并行编程模型。
要启用 OpenMP 指令,请用 -openmp 选项标志进行编译。Fortran 95 OpenMP 指令用类似注释的 !$OMP 标记标识,标记后紧跟指令名和从属子句。
!$OMP PARALLEL 指令标识程序中的并行区域。!$OMP DO 指令标识并行区域内即将并行化的 DO 循环。可以将这些指令合并成单个 !$OMP PARALLEL DO 指令,该指令必须紧跟在 DO 循环之前。
OpenMP 规范包括许多用于在程序并行区域中共享和同步工作的指令,还包括用于数据作用域和控制的从属子句。
OpenMP 和传统的 Sun 风格指令之间的一个主要不同点是,OpenMP 需要显式数据作用域以专用或共享方式使用,并还要提供自动作用域功能。
有关更多信息(包括使用 Sun 和 Cray 并行化指令转换传统程序的指导原则),请参见《OpenMP API 用户指南》。
并行化使用多个环境变量:OMP_NUM_THREADS、SUNW_MP_WARN、SUNW_MP_THR_IDLE、SUNW_MP_PROCBIND、STACKSIZE 和其他环境变量。《OpenMP API 用户指南》中对它们进行了介绍。
real x / 1.0 /, y / 0.0 / print *, x/y end character string*5, out*20 double precision value external exception_handler i = ieee_handler(’set’, ’all’, exception_handler) string = ’1e310’ print *, ’Input string ’, string, ’ becomes: ’, value print *, ’Value of 1e300 * 1e10 is:’, 1e300 * 1e10 i = ieee_flags(’clear’, ’exception’, ’all’, out) end integer function exception_handler(sig, code, sigcontext) integer sig, code, sigcontext(5) print *, ’*** IEEE exception raised!’ return end |
运行时输出:
调试已并行化的程序需要做一些额外工作。下列方案提出了处理该任务的方法。
有一些步骤可以直接进行尝试以确定错误原因。
关闭并行化。
可以采取下列某一步骤:
关闭并行化选项-用 -O3 或 -O4 选项进行编译但不使用任何并行化选项来验证程序是否正确工作。
将线程数设置为 1,然后打开并行化选项进行编译-将环境变量 PARALLEL 设置为 1 来运行程序。
如果问题消失,则可以假定问题是由于使用多线程而引起的。
另外,通过用 -C 进行编译,检查数组引用是否越界。
使用带 —autopar 选项的自动并行化问题可能会指示编译器正在并行化它本来不应并行化的应用程序。
关闭 -reduction。
如果正在使用 —reduction 选项,可能会进行求和约简,并得出稍微不同的答案。尝试不用该选项运行。
使用 fsplit。
如果程序中有许多子例程,可用 fsplit(1) 将它们拆成单独的文件。然后使用或不使用—autopar 编译一些文件。
执行二进制文件并验证结果。
重复该过程直到将问题范围缩小至一个子例程为止。
使用 -loopinfo。
检查哪些循环正在进行并行化、哪些循环未进行并行化。
使用伪子例程。
创建一个不执行任何操作的伪子例程或函数。将该子例程的调用置于几个正在进行并行化的循环内。重新编译并执行。使用 -loopinfo 查看哪些循环正在进行并行化。
继续该过程直到开始获得正确的结果。
逐次反向运行循环。
将 DO I=1,N 替换为 DO I=N,1,-1。如果结果不同,则表明存在数据依赖性。
避免使用循环索引。
替换:
DO I=1,N ... CALL SNUBBER(I) ... ENDDO |
使用:
DO I1=1,N I=I1 ... CALL SNUBBER(I) ... ENDDO |
下列书籍提供更多的信息:
《OpenMP API 用户指南》
由 Rajat Garg 和 Ilya Sharapov 合著的《Techniques for Optimizing Applications: High Performance Computing》,Sun Microsystems Press Blueprint 出版,2001。
由 Kevin Dowd 和 Charles Severance 合著的《High Performance Computing》,O’Reilly and Associates 出版,第二版,1998。
由 Rohit Chandra et al 编著的《Parallel Programming in OpenMP》,Morgan Kaufmann Publishers 出版,2001。
由 Barry Wilkinson 编著的《Parallel Programming》,Prentice Hall 出版,1999。