归并的OpenMP并行程序

深圳大学  并行计算  实验五

一、实验目的

1. 设计归并的并行算法;

2. 掌握数据划分方法;

3. 对并行程序进行简单的性能分析。

二、实验环境

1. 硬件环境:64核CPU、256GB内存的共享内存并行计算平台;

2. 软件环境:Ubuntu Linux、gcc、g++(g++ -O3 -fopenmp -o a.out a.cpp);

3. 远程登录:本地PowerShell中执行ssh bxjs@hpc.szu.edu.cn;

4. 传输文件:本地PowerShell中执行scp c:\a.cpp bxjs@hpc.szu.edu.cn:/home/bxjs/ftp://hpc.szu.edu.cn

三、实验内容

1. 用OpenMP语言编写程序,将有序数组ab归并到数组c中。将数组a等分为p段,并选前p-1段的最后一个元素作为划分元,将数组b也分为p段;线程i归并数组ab的第i段。为了验证结果正确性,将并行计算结果和串行计算结果相比较。

代码实现思路:

首先,我们考虑用构建两个长度为N的double类型的一维数组a、b,同时构建两个长度为N*2的double类型的一维数组c、d,其中c数组用来存储数组a、b并行归并的结果,而d数组用来存储数组a、b串行归并的结果。

接着,我们考虑对数组a、b进行随机的初始化,同时,要保证数据有序性。对此,我采用了cstdlib库中rand()函数获取一个0到RAND_MAX之间的随机整数,再将随机数乘以1.0变成double类型,再除以RAND_MAX,最终获得一个0到1之间的随机的浮点数,然后加上一个数组元素的值,这样就可以获得两个随机的有序的double类型的一维数组。

在完成初始化后,我考虑使用了OpenMP库中,omp_set_num_threads(int tp)函数用于设置并行区域的线程数为tp。再omp_get_wtime()函数,获取当前线程距离线程线程开始的时间差,赋值给double类型的变量t0,作为并行域计算的起始时间。

       然后,我考虑了通过一个for循环将数组a分成thread_num(thread_num为进行线程数)个小数组。这里在进行分小组的操作是,采用的是计算前后指针,然后进行存储和记录,来实现的。然后,将前thread_num-1个子数组的最后一个元素作为划分元。

       为了实现并行归并,还需要将数组b分成thread_num(thread_num为进行线程数)个小数组,但是这个不能简单采用前面的a数组的划分办法,因为如果采用这种划分办法对b数组进行划分,会导致最后的分段归并的结果还需要再归并才能保持有序,这样作就没有意义了。

所以这里对b数组采用的划分办法是:根据前面a数组的前thread_num-1个子数组的划分元,在b数组中查找比这个thread_num-1划分元刚好小于的最大元素,以此将b数组划分为thread_num个子数组。而对于这个查找的过程,考虑采用并行来实现,所以我使用了“#pragma omp parallel shared(a,b,a_begin,a_end,b_begin,b_end,thread_num)编译指导语句,构建一个并行域,在并行域中使用了OpenMP库中的omp_get _threads_num ()函数来获取当前线程的线程号,然后依据该线程号,进行对应的并行查找。同时考虑采用b数组的有序性,这里考虑采用C++标准库<algorithm>中的lower_bound函数,进行二分查找,获取相应的下标。

之后,为了保证a数组中的子数组和b数组中的子数组在进行并行归并的时候,可以将数据归并到c数组中对应的位置,我这里采用了一个for循环,计算每段归并需要存储在c数组的什么位置,采用的是计算前后指针,然后进行存储和记录。

接着,使用“#pragma omp parallel shared(a, b, c, a_begin, a_end, b_begin, b_end, c_begin, c_end)”编译指导语句,构建一个并行域,在并行域中使用了OpenMP库中的omp_get _threads_num ()函数来获取当前线程的线程号,然后依据该线程号,让每个线程将对应的ab子数组进行归并,存储到c数组中对应的位置。这里可以直接使用C++ 标准库<algorithm>中的merge函数完成归并操作。

接着,再次使用omp_get_wtime()函数,获取当前线程距离线程线程开始的时间差,赋值给double类型的变量t1,作为并行域计算的结束时间。继而,通过t1-t0就可以获得该并行域的运行时间。

最后,直接使用merge函数将完整的a数组和b数组归并到d数组中,实现串行归并。然后,通过一个for循环进行遍历,判断c数组和d数组的结果是否一致,来判断并行运行的正确性。

初步代码实现:

const int N=100000000;//求解范围 
double a[N],b[N],c[N*2],d[N*2];//a为并行结果、b为串行验证结果
//初始化参数 
srand(time(NULL));
for (int i = 0; i < N; i++){
	a[i] = (i ? a[i-1] : 0) + rand() * 0.1 / (double)RAND_MAX ;
	b[i] = (i ? b[i-1] : 0) + rand() * 0.1 / (double)RAND_MAX ;
}
double t0,t1;
t0 = omp_get_wtime();
omp_set_num_threads(thread_num);
//begin、end为分段数组的前后指针,  c_begin、c_end为存储c数组的前后指针 
int a_begin[thread_num],a_end[thread_num];
int b_begin[thread_num],b_end[thread_num];
int c_begin[thread_num],c_end[thread_num];
//分成 thread_num 个子数组  
for (int i = 0; i < thread_num; i++){
	a_begin[i] = i ? a_end[i-1] + 1 : 0;
	a_end[i] = (i == thread_num - 1 ? N - 1 : (N/thread_num) * (i+1));
}
//并行代码 
#pragma omp parallel shared(a,b,a_begin,a_end,b_begin,b_end,thread_num)
{
	int nthreads = omp_get_thread_num();
	b_end[nthreads] = lower_bound(b,b+N,a[a_end[nthreads]]) - b - 1;
	if(nthreads == thread_num-1) b_end[nthreads] = N-1 ;
}
for (int i = 0; i < thread_num; i++){
	b_begin[i] = i ? b_end[i-1] + 1 : 0;
	c_begin[i] = i ? c_end[i-1] + 1 : 0;
	c_end[i] = c_begin[i] + b_end[i] - b_begin[i] + 1 + a_end[i] - a_begin[i];
}
#pragma omp parallel shared(a,b,c,a_begin,a_end,b_begin,b_end,c_begin,c_end)
{
	int nthreads = omp_get_thread_num();
	merge(a+a_begin[nthreads], a+a_end[nthreads]+1, b+b_begin[nthreads], b+b_end[nthreads]+1, c+c_begin[nthreads]);
}
t1 = omp_get_wtime();
//串行排序代码 
merge(a, a+N, b, b+N, d);
//验证代码
int flag=1;
for(int i = 0; i < N*2; i++)
	if(c[i] != d[i]){
		flag=0;
		cout << "Results are not equal!" << endl;
		break;
	}	
if(flag) 
	cout << "Results are equal!" << endl;
cout << "Thread num: "<<thread_num<<", Parallel time: " << t1 - t0 <<"second"<< endl;

2. 测试并行程序在不同线程数下的执行时间和加速比(与线程数=1时的执行时间相比)。其中,n固定为100000000,线程数分别取1、2、4、8、16、32、64时,为减少误差,每项实验进行5次,取平均值作为实验结果。

为了避免代码多次修改,这里可以将对上述1中的代码进行封装成函数“double achieve(int thread_num)”,传入参数thread_num,为程序并行域的线程数,并将并行域的运行时间作为返回值进行返回。

同时,为了避免程序的多次间断的运行,导致程序运行时CPU的状态差,导致实验获取的结果误差较大,这里考虑,创建数组提前存储“1、2、4、8、16、32、64”,然后通过for循环将数值传入函数“double juzheng(int thread_num)”中,获取运行时间,并计算出对应5次实验的平均值。从而,避免了程序的多次间断的运行带来的误差。具体的代码在下列“四、代码描述”中可见。

四、代码描述

如下所示,为该实验的完整实现代码,必要的一些参数主要通过改变全局变量的参数进行设置,而主函数通过循环调用double achieve(int thread_num) 函数,获取对应thread_num下的平均的并行域的运行时间,然后打印出来。

在double achieve(int thread_num)函数中,主要通过“#pragma omp parallel shared(a, b, a_begin, a_end, b_begin, b_end, thread_num)”和“#pragma omp parallel shared(a, b, c, a_begin, a_end, b_begin, b_end, c_begin, c_end)”实现的两个并行域实现并行操作。

在“#pragma omp parallel shared(a, b, a_begin, a_end, b_begin, b_end, thread_num)”这个并行域内实现对b数组的分割。在“#pragma omp parallel shared(a, b, c, a_begin, a_end, b_begin, b_end, c_begin, c_end)”实现对thread_num个子数组对的并行归并。

    通过分析,实现a数组和b数组的并行归并中,其中串行部分的对a数组的划分和确定c数组存储位置,都是通过一个for循环实现,时间复杂度是O(thread_num) ,是常数级比,基本可以忽略不记。而对b数组进行并行分割,理论上需要的时间就是一个二分查找的时间,为O(logN) ;而最后对thread_num个子数组对进行归并,理论上如果b数组的划分比较均匀的话,需要的时间是 次加法的时间。所以,总共完成将有序数组a和有序数组b进行并行归并,需要的时间是

#include <omp.h>
#include <bits/stdc++.h>
using namespace std;
const int N=100000000;//求解范围 
const int ThreadNum_CanShu=7;//线程数组的大小
const int set_thread_num[]={1,2,4,8,16,32,64};//线程数
const int XunHuan_CanShu=5;//循环次数 
double a[N],b[N],c[N*2],d[N*2];//a为并行结果、b为串行验证结果 
//根据传入的线程数,进行函数的调用 
double achieve(int thread_num){
	//初始化参数 
	srand(time(NULL));
	for (int i = 0; i < N; i++){
		a[i] = (i ? a[i-1] : 0) + rand() / (double)RAND_MAX ;
		b[i] = (i ? b[i-1] : 0) + rand() / (double)RAND_MAX ;
	}
	double t0,t1;
	//begin、end为分段数组的前后指针,  c_begin、c_end为存储c数组的前后指针 
	int a_begin[thread_num],a_end[thread_num];
	int b_begin[thread_num],b_end[thread_num];
	int c_begin[thread_num],c_end[thread_num];
	//分成 thread_num 个子数组  
	for (int i = 0; i < thread_num; i++){
		a_begin[i] = i ? a_end[i-1] + 1 : 0;
		a_end[i] = (i == thread_num-1 ? N-1 : (N/thread_num)*(i+1));
	}
	omp_set_num_threads(thread_num);
	t0 = omp_get_wtime();
	//并行代码 
	#pragma omp parallel shared(a,b,a_begin,a_end,b_begin,b_end,thread_num)
	{
		int nthreads = omp_get_thread_num();
		b_end[nthreads]=lower_bound(b,b+N,a[a_end[nthreads]])-b-1;
		if(nthreads == thread_num-1) b_end[nthreads] = N-1 ;
	}
	for (int i = 0; i < thread_num; i++){
		b_begin[i] = i ? b_end[i-1] + 1 : 0;
		c_begin[i] = i ? c_end[i-1] + 1 : 0;
		c_end[i]=c_begin[i]+b_end[i]-b_begin[i]+1+a_end[i]-a_begin[i];
	}
	#pragma omp parallel shared(a,b,c,a_begin,a_end,b_begin,b_end,c_begin,c_end)
	{
		int nthreads = omp_get_thread_num();
		merge(a+a_begin[nthreads], a+a_end[nthreads]+1, b+b_begin[nthreads], b+b_end[nthreads]+1, c+c_begin[nthreads]);
	}
	t1 = omp_get_wtime();
	//串行排序代码 
	merge(a, a+N, b, b+N, d);
	//验证代码
	int flag=1;
	for(int i = 0; i < N*2; i++)
		if(c[i] != d[i]){
			flag=0;
			cout << "Results are not equal!" << endl;
			break;
		}	
	if(flag)  cout << "Results are equal!" << endl;
	cout << "Thread num: "<<thread_num;
	cout << ", Parallel time: "<< t1 - t0 <<"second"<< endl;
	return t1-t0;
}
int main()
{
	cout<<"CPU核心数: "<<omp_get_num_procs()<<endl;
	cout<<"最大线程数: "<<omp_get_thread_limit()<<endl; 
	cout<<"----------------------------------"<<endl; 
	for(int i=0;i<ThreadNum_CanShu;i++){
		double pingjunzhi=0,speedup_rate=0;
		for(int j=0;j<XunHuan_CanShu;j++)
			pingjunzhi+=achieve(set_thread_num[i]);
		cout<<"omp_set_num_threads="<<set_thread_num[i];
		cout<<", 平均运行时间:"<< pingjunzhi/XunHuan_CanShu;
		cout<<"秒"<<endl;;
		cout<<"----------------------------------"<<endl; 
	}
	return 0;
}

五、实验结果和分析

通过在文件目录下输入“ftp://hpc.szu.edu.cn”,使用ftp协议,将本地的b.ccp的代码文件传输到“/2021150233”的目录下。然后,打开shell窗口,使用“ssh bxjs@hpc.szu.edu.cn”连接A0服务器,但是由于A1服务器只有32个核,而A0有64个核,所以下面的实验数据在A0服务器上执行获得的。

通过cd命令打开“/202115023”文件夹,然后通过“g++ -O3 -fopenmp -o b.out b.cpp”将b.ccp文件编译为b.out可执行文件。然后通过“./b.out”运行b.out文件,最终执行的结果如下图1所示,并将执行的结果记录进表1中,同时计算出加速比(与线程数=1时的执行时间相比)

 

图1  b文件的执行结果

表1 并行程序在不同线程数下的执行时间(秒)和加速比n=100000000

线程数

执行时间/s

1

2

4

8

16

32

64

第1次

4.70544

3.11984

1.86865

1.18653

0.69979

0.54144

0.37931

第2次

5.45614

2.99073

1.94239

1.10194

0.87540

0.44713

0.42043

第3次

5.45514

2.97473

1.72461

1.10098

0.73548

0.51287

0.32265

第4次

5.68466

3.03499

1.93830

1.16766

0.74305

0.52182

0.40450

第5次

5.68366

2.95000

1.93609

1.09127

0.80166

0.51306

0.32882

平均值

5.39701

3.01406

1.88201

1.12968

0.77108

0.50726

0.37114

加速比

1.00000

1.79061

2.86768

4.77747

6.99993

10.63953

14.54171

根据上述表1的平均值和加速比数据,绘制出下面的折线图:

图2  平均值和加速比的折线图

根据对上述,表1和图2的分析可知,随着线程数的增加,求长度为n=100000000的数组的前缀和,需要的时间在逐渐下降,加速比逐渐上升。即,在CPU核心数量的范围内,通过增加线程数,分割任务进行并行归并可以实现对复杂归并的优化。

同时,观测到加速比不是简单随线程数的增加而线性增加的,两者不是简单的正比关系,加速比随着线程数的增加,但是增长越来越缓慢了。这与前面代码描述部分分析的“总共完成将有序数组a和有序数组b进行并行归并,需要的时间是 O(2×Nthread_num+logN+thread_num×2) ”有关系,执行时间与线程数并不是简单的反比关系。

除此之外,在对b数组进行并行分割时,很多时候可能是并不能实现较为均匀的分割的,所以这也在很大程序上影响着程序的运行时间。并且随着线程数的增加,CPU对任务调度的开销也随之增加,导致加速效果也在一定程度上被削弱,最终导致两者呈现出这种关系。

六、实验结论

在准备实验的初期,我对如何使用并行算法来实现并行归并感到困惑重重。我知道有序数组的归并是一个常见的计算任务,但是如何将它与并行化算法结合起来并不是一件简单的事情。这个问题让我感到迷茫,因为我缺乏足够的经验和理解。

但是,在我认真上完课上对该问题的讲解后,豁然开朗,逐步掌握了如何将有序数组的归并分解成多个并行子任务,并利用多核CPU资源进行加速的思路。然后,在上手写代码实现的过程中,我也遇到了不少挑战和问题。但是,每当我遇到困难时,我都会认真思考,并寻找解决办法。通过不断地尝试和调试,我逐步解决了这些问题,最终成功地实现了前缀和的并行算法。

在本次实验中, 首先,在算法设计上,我们采用了归并排序的并行算法,并利用OpenMP语言编写了程序。通过将有序数组a和b进行归并,利用并行计算平台的多核CPU并行处理任务,从而提高了算法的效率。

其次,在数据划分方法上,我们对数组a和b进行了合理的划分,以便在并行归并过程中各个线程能够独立地处理各自的子数组。通过合理的划分方法,我们最大程度地利用了并行计算平台的资源,提高了并行算法的效率。

另外,我们进行了简单的性能分析,通过对不同线程数下程序的执行时间进行测试,并计算了加速比。实验结果显示随着线程数的增加,程序的执行时间逐渐减少,加速比逐渐增加。这表明我们设计的并行算法在一定程度上具有良好的性能表现。

通过实验,我们可以知道:利用并行算法可以显著提高排序算法的执行效率,特别是在大规模数据处理场景下;随着线程数的增加,程序的执行时间呈现出逐渐减少的趋势,但增长越来越缓慢;在并行计算中,合理的数据划分方法对程序的性能影响巨大,需要根据实际情况进行优化;考虑到并行计算中存在的任务调度开销,加速比不是简单的线性增长,而是会逐渐趋于平缓。

综上所述,本次实验通过并行算法设计与实现,充分利用了计算资源,提高了算法的执行效率,并从实验结果中得出了一些有益的结论,对进一步优化并行算法的设计与实现具有一定的指导意义。

  • 18
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值