使用 -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《数值计算指南》。