关于OpenMP的循环算法
在编写并行程序时,我们一般都是要将所需完成的任务分配给各个核————即将任务并行化。但是很多时候,很多我们熟悉的串行算法并不容易并行化,比如经典的冒泡排序:
for(length = n; length >= 2; length--)
{
for(i=0; i < length-1; i++)
{
if(a[i] > a[i+1])
{
tmp = a[i];
a[i] = a[i+1];
a[i+1] = tmp;
}
}
}
不难看出,这个算法有着很明显的内外循环依赖。拿外部循环举例,在外部循环的每一次迭代中,当前的内容依赖于外部循环的前一次迭代,即与上一次迭代的结果有着非常紧密的联系。如果我们在此基础上强行将其并行话的话,一般只能得到两个结果——要么参与运行的核依次等待上一个核完成任务,程序变得与串行化无异;要么参与计算的核各自为战,导致循环混乱,得出错误的答案——这都是我们不想看到的。
更易并行化的奇偶排序法
在进行接下来的叙述之前,我们先来了解一下什么是奇偶排序法:
比如我们随机列出七个数字:
之后我们以偶数位为基准,将每一个偶数位的数以及与它右边相邻的数划为一组:
之后在组内比较大小,按照大小关系排序:
之后又以奇数位为基准,将每一个奇数位的数以及与它右边相邻的数划为一组:
同理将其进行排序:
之后一直按照“偶数位确定组并排序->奇数位确定组并排序”这样的循环进行排序,直到总体顺序符合要求:
由此看来,奇偶排序法外部仍存在一个循环依赖,但是内部并没有循环依赖,可以将数据分组做并行比较处理。因此,奇偶排序算法具有很好的并行化潜力。
奇偶变换排序的并行算法实现
为方便验证我先创建一个打乱顺序的数组:
const int N = 10;
int a[N] = {10, 8, 79, 55, 13, 2, 45, 68, 11, 7};
之后先明确先决条件:
int phase,tmp,i;
# pragma omp parallel num_threads(4) default(none) shared(a,N) private(i,tmp,phase)
/*num_threads(4):明确使用4个线程,可以另作修改
default(none) shared(a,N):明确共享的变量,以随时更新所需的公有数据
private(i,tmp,phase):确定每个线程的私有变量,避免线程间数据混淆冲突
之后是算法主体:
for(phase = 0; phase < N; phase++){
if (phase % 2 == 0) //偶数情况
{
# pragma omp for //内部for循环没有循环依赖,可以将其并行化
for(i = 1; i < N; i += 2) //所有可行偶数位及右相邻数字进行比较
{
if(a[i-1] > a[i]) //简单组内比较大小
{
tmp = a[i-1];
a[i-1] = a[i];
a[i] = tmp;
}
}
}
else{ //奇数情况
# pragma omp for
for(i = 1; i < N-1; i += 2) //所有可行奇数位及右相邻数字进行比较
{
if(a[i] > a[i+1])
{
tmp = a[i+1];
a[i+1] = a[i];
a[i] = tmp;
}
}
}
}
由此经过排序,只需一个简单的输出函数即可完成。
for(i = 0;i < N; i++)
{
printf("%d ",a[i]);
}
原代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
const int N = 10;
int main()
{
int i,temp,phase;
int a[N] = {10, 8, 79, 55, 13, 2, 45, 68, 11, 7};
# pragma omp parallel num_threads(4) default(none) shared(a, N) private(i, temp, phase)
for(phase = 0; phase < N; phase++)
{
if(phase % 2 == 0)
{
# pragma omp for
for(i = 1; i< N; i += 2)
{
if(a[i-1] > a[i])
{
temp = a[i-1];
a[i-1] = a[i];
a[i] = temp;
}
}
}
else
{
# pragma omp for
for(i = 1; i < N-1; i += 2)
{
if(a[i] > a[i+1])
{
temp = a[i+1];
a[i+1] = a[i];
a[i] = temp;
}
}
}
}
for(i = 0; i < N; i++)
{
printf("%d ",a[i]);
}
return 0;
}
总而言之,奇偶排序法是对刚接触并行计算的新手而言比较友好的一种算法,它将数据模块化分而处理的方法很适合用于并行计算,其中心思想也对我们日后进行并行计算任务的划分有相应的启示。当然,其实冒泡排序法也是可以通过改造并行化的,但是步骤相比此方法而言就会麻烦一点,日后我会再介绍。