浅谈OpenMP的奇偶排序法(C语言)

1,480 阅读3分钟

关于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;
        }
    }
}

不难看出,这个算法有着很明显的内外循环依赖。拿外部循环举例,在外部循环的每一次迭代中,当前的内容依赖于外部循环的前一次迭代,即与上一次迭代的结果有着非常紧密的联系。如果我们在此基础上强行将其并行话的话,一般只能得到两个结果——要么参与运行的核依次等待上一个核完成任务,程序变得与串行化无异;要么参与计算的核各自为战,导致循环混乱,得出错误的答案——这都是我们不想看到的。

QQ截图20220317140522.png

更易并行化的奇偶排序法

在进行接下来的叙述之前,我们先来了解一下什么是奇偶排序法:

比如我们随机列出七个数字:

1.png

之后我们以偶数位为基准,将每一个偶数位的数以及与它右边相邻的数划为一组:

2.png

之后在组内比较大小,按照大小关系排序:

QQ截图20220317140648.png 之后又以奇数位为基准,将每一个奇数位的数以及与它右边相邻的数划为一组:

QQ截图20220317140716.png

同理将其进行排序:

5.png

之后一直按照“偶数位确定组并排序->奇数位确定组并排序”这样的循环进行排序,直到总体顺序符合要求:

6.png

由此看来,奇偶排序法外部仍存在一个循环依赖,但是内部并没有循环依赖,可以将数据分组做并行比较处理。因此,奇偶排序算法具有很好的并行化潜力。

QQ截图20220317140752.png

奇偶变换排序的并行算法实现

为方便验证我先创建一个打乱顺序的数组:

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;
}

总而言之,奇偶排序法是对刚接触并行计算的新手而言比较友好的一种算法,它将数据模块化分而处理的方法很适合用于并行计算,其中心思想也对我们日后进行并行计算任务的划分有相应的启示。当然,其实冒泡排序法也是可以通过改造并行化的,但是步骤相比此方法而言就会麻烦一点,日后我会再介绍。