并行计算:循环程序并行化的一般方法
一、数据划分和处理器指派
1. 带状划分方法
又叫做行列划分,就是将矩阵的整行或整列分成若干组,各组指派给一个处理器。
例如:设矩阵A由n行和m列,对其串行处理的程序段如下:
for i=1 to n do
for j=1 to m do
Process(a[i,j])
endfor
endfor
其中Process(a[i,j])表示对矩阵元素a[i,j]某种处理过程。
(1)行划分
现在有p个处理器,可以并行的去处理。那么在行划分的情况下,每个处理器需要处理r=n/p行(假设能够整除),那么程序的实际运行是下面这样的:
for k=1 to p parallel-do
for i=1 to r do
for j=1 to m do
Process(a[(k-1)r+i,j])
endfor
endfor
endfor
假设四个处理器处理16x16的矩阵,那么分配情况如下
处理器 | 行 |
1 | 0,1,2,3 |
2 | 4,5,6,7 |
3 | 8,9,10,11 |
4 | 12,13,14,15 |
(2)列划分
现在有p个处理器,可以并行的去处理。那么在列划分的情况下,每个处理器需要处理s=m/p行(假设能够整除),那么程序的实际运行是下面这样的:
for k=1 to p parallel-do
for j=1 to s do
for i=1 to n do
Process(a[i,(k-1)s+j])
endfor
endfor
endfor
处理器 | 列 |
1 | 0,1,2,3 |
2 | 4,5,6,7 |
3 | 8,9,10,11 |
4 | 12,13,14,15 |
(3)行循环划分
行循环划分跟划分的区别在于行划分一块一块地划分,而行循环划分是循环地划分。在行划分地情况下第一个处理器可能分到的是0,1,2,3行,而在行循环划分则是0,4,8,12(假设有四个处理器)
for k=1 to p parallel-do
for i=1 to r do
for j=1 to m do
Process(a[(i-1)p + k, j])
endfor
endfor
endfor
处理器 | 行 |
1 | 0,4,8,12 |
2 | 1,5,9,13 |
3 | 2,6,10,14 |
4 | 3,7,11,15 |
(4) 列循环划分
for k=1 to p parallel-do
for i=1 to n do
for j=1 to s do
Process(a[i, (j-1)p+k])
endfor
endfor
endfor
处理器 | 列 |
1 | 0,4,8,12 |
2 | 1,5,9,13 |
3 | 2,6,10,14 |
4 | 3,7,11,15 |
2. 块状划分方法
如使用棋盘划分的方法。
3.数据划分准则
(1) 并行粒度准则
如果某一项给定的任务在其完成后要求同步时的最坏事件复杂度为t(n), 那么最大可能加速比是
如复杂度为n^3, 则最大可能加速比为n^1.5, 所以我们最求并行粒度更大一些,以最求更大的加速比。
(2)数据相关性准则
划分应该减少处理器之间的通信,即处理器之间的数据相关性越弱越好。
二、循环重构
1.循环交换
即行循环和列循环交换。如果行与行之间存在数据依赖(称为行相关),或者列与列之间有数据依赖(列相关),可以通过循环交换来实现并行化。
for i=1 to n do
for j=1 to n do
a[i,j]=a[i-1,j]
endfor
endfor
可以转化为
for j=1 to n parallel-do
for i=1 to n do
a[i,j] = a[i-1,j]
endfor
endfor
2.拉伸法
如果既有行相关,又有列相关,那么可以通过拉伸来解决数据依赖,并实现并行化。
for i=1 to n do
for j=1 to n do
a[i,j] = a[i-1,j]+a[i,j-1]
endfor
endfor
拉伸之后,拉伸系数为1
for i=1 to n do
for j=i+1 to i+n do
a[i,j-i]=a[i-1,j-i]+a[i,j-i-1]
endfor
endfor
这样,我们可以进行列的并行化,为了适应实际需要(数据范围的变化),上面的程序应该修改为。
for j=2 to 2n do
for i=max(1,j-n) to min(n, j+1) parallel-do
a[i,j-i] = a[i-1,j-i] + a[i,j-i-1]
endfor
endfor
这里只能内层做并行化,因为问题本身既有行相关又有列相关,拉伸后消除了行相关,从而能对其中一层进行并行化。虽然并行粒度较小,但比原来不能进行并行化的情况要快。
这里再举一个例子。
设A为n阶矩阵,有串行程序如下:
for i=1 to n do
for j=1+i to n+i do
a[i,j] = (a[i,j+1]+a[i,j-1]+a[i+1,j]+a[i-1,j])/4
endfor
endfor
所以我们可以构造并行程序如下所示
for j=2 to 2n do
for i=max(1,j-n) to min(n,j-1) parallel-do
a[i,j-i] = (a[i,j+1-i]+a[i,j-1-i]+a[i+1,j-i]+a[i-1,j-i])/4
endfor
endfor
这里不在计算范围的数据如a[0]设为0。这里主要的技巧即使坐标的转换,当我们进行斜向计算的时候,可以放下,每一个数据它们的横纵坐标x,y是符合x+y=2....2n的,所以得出a[i,j-i]。
3.分裂法
分裂法是一种通过将循环迭代空间细分称相同形状和大小的数据块以达到局部化相关关系的变换方法。变换之后,一个子数据块内部的全部迭代被同一个处理器执行,只有在子数据块之间计算才需要通信。
这里,我们通过分裂法,把n阶矩阵分成了大小为ss*ss的小矩阵块进行并行计算,只有当C的计算需要进行块与块之间通信时才会进行通信。
4.轮转法
轮转法的做法与拉伸法很类似,对循环j和循环i进行拉伸,拉伸系数为i。但是它的处理顺序和拉伸法不同。
//重构前
for i=0 to n-1 do
for j=0 to m-1 do
Process(a[i,j])
endfor
endfor
//重构之后
for i=0 to n-1 do
for j=0 to m-1 parallel-do
Process(a[i,(j+i)mod m])
endfor
endfor
5. 并列法
并列法将循环体中无相关关系的部分拆分称几个并列的循环,使这几个循环之间可以互相并行执行。如下所示:
for i=1 to n do
for j=1 to n do
S1;S2;...;Sm
endfor
endfor
这时我们可以对S1,S2进行拆分
for k=1 to m parallel-do
for i=1 to n do
for j=1 to n do
Sk
endfor
endfor
endfor
这样子划分的话,它的加速比可以提升更大,如果在原基础上进行行的并行化或者列的并行化,可能还会存在数据依赖的问题,且加速比不够大。
推荐阅读