前缀和的OpenMP并行程序

深圳大学  并行计算  实验四

一、实验目的

1. 设计前缀和的并行算法;

2. 掌握for编译制导语句和数据划分方法;

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语言编写程序,求数组a的前缀和,保存在数组b中,即b[i]=0≤k≤ia[k]

。为了验证结果正确性,将并行计算结果和串行计算结果相比较,误差在1e-4以内。

代码实现思路:

首先,我们考虑用构建两个double类型的一维数组a、b。接着,我们考虑对数组a进行随机的初始化,对此,我采用了cstdlib库中rand()函数获取一个0到RAND_MAX之间的随机整数,再将随机数乘以1.0变成double类型,再除以RAND_MAX,最终获得一个0到1之间的随机的浮点数。

       然后,我考虑了将数组a分成thread_num(thread_num为进行线程数)个小数组。这里在进行分小组的操作是,采用的是计算前后指针,然后进行存储和记录,来实现的。

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

接着,使用“#pragma omp parallel shared(a,begin,end)”编译指导语句,构建一个并行域,在并行域中使用了OpenMP库中的omp_get _threads_num ()函数来获取当前线程的线程号,然后依据该线程号,让每个线程各自执行对应的子求前缀和的操作,实现thread_num个小数组的进行求前缀和,并将结果存储到b数组中对应的位置。

在完成并行求thread_num个小数组的进行求前缀和后,只有第一个子数组的前缀和结果是正确,而后面的thread_num-1个小数组的前缀还是不正确的。此时,还需要让这thread_num-1个小数组的前缀结果加上前一个子数组末尾的正确的前缀和,才能使得这个thread_num-1个小数组的前缀结果变成正确的结果。这个部分操作,也可以考虑并行地执行。

接着,我考虑使用了一个for循环,来分别对这thread_num-1个小数组从前往后加上前一个子数组末尾的正确的前缀和。而在这个for循环内,为这个子数组的加法操作的速度,考虑让多个线程并行执行,先用变量temp存储前一个子数组末尾正确的前缀和的值。

然后,我使用“#pragma omp parallel for shared(a,begin,end) schedule(dynamic,10000)”编译指导语句,构建一个for循环并行域,在该并行域内执行“b[i]+=temp”的操作。

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

最后,通过使用一个for循环,串行实现a数组的求前缀和的操作,并将结果存储到a数组中。同时,将串行求取的结果与b数组中的值进行比较,判断是否相等,从而来判断矩阵乘法并行化的正确性。但是,考虑到数组的数据是浮点类型,所以在判断c矩阵和d矩阵的中数值是否相等时,为了避免精度丢引起判断错误,只需保证误差的一定小的范围内1e-4,即可认为数值相等。

初步代码实现:

//初始化参数 
srand(time(NULL));
for (int i = 0; i < N; i++)
	a[i] = rand()*1.0/(double)RAND_MAX;//0 ~ (RAND_MAX)32767
double t0,t1,t2,t3;
int begin[thread_num],end[thread_num],num;
//分成 thread_num 个子数组  
for (int i = 0; i < thread_num; i++){
	if(i == 0) 
		begin[0] = 0;
	else 
		begin[i] = end[i-1] + 1;
	if(i == thread_num - 1)
		end[i] = N - 1;
	else
		end[i] = (N/thread_num) * (i+1);
}
omp_set_num_threads(thread_num);
t0 = omp_get_wtime();
//并行——小数组求前缀和,对 thread_num 个子数组进行并行求前缀和
#pragma omp parallel shared(a,begin,end)
{
	int nthreads = omp_get_thread_num();
	b[begin[nthreads]]=a[begin[nthreads]];
	for(int i=begin[nthreads]+1;i<=end[nthreads];i++){
		b[i]=b[i-1]+a[i];
	}
}
//并行——对后面的 thread_num-1 个子数组,加上前一个子数组的末尾值 
for(int i = 1; i < thread_num; i++){
	double temp = b[end[i-1]];  
	#pragma omp parallel for shared(begin,end,i,temp) schedule(dynamic,10000)
	for(int j = begin[i]; j <= end[i]; j++){
		b[j] += temp;
	}
}
t1 = omp_get_wtime();
//串行验证代码
int flag=1;
for(int i = 0; i < N; i++){
	if(i != 0) 
		a[i] += a[i-1];
	if(abs(b[i]-a[i])>1e-4){
		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 for shared(a,begin,end)”和“#pragma omp parallel for shared(a,begin,end) schedule(dynamic,10000)”实现的两个并行域实现并行操作。

在“#pragma omp parallel for shared(a,begin,end)”这个并行域内实现thread_num个子数组的求前缀和的操作。在“#pragma omp parallel for shared(a,begin,end) schedule(dynamic,10000)”实现对thread_num-1个子数组的加上前一个子数组末尾的前缀和的操作。

       通过分析,并行求数组a的前缀和的操作中,thread_num个子数组的求前缀和,理论上需要的时间是计算  次加法的时间;对thread_num-1个子数组的加上前一个子数组末尾的前缀和的操作,理论上需要的时间是计算  次加法的时间。所以,总共并行完成数组a求前缀和操作,需要的时间是 次加法的时间。

#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];//a为初始化值、b为并行结果、a为串行验证结果 
//根据传入的线程数,进行函数的调用 
double achieve(int thread_num){
	//初始化参数 
	srand(time(NULL));
	for (int i = 0; i < N; i++)
		a[i] = rand()*1.0/(double)RAND_MAX;//0 ~ (RAND_MAX)32767
	double t0,t1,t2,t3;
	int begin[thread_num],end[thread_num],num;
	//分成 thread_num 个子数组  
	for (int i = 0; i < thread_num; i++){
		if(i == 0)  begin[0] = 0;
		else  begin[i] = end[i-1] + 1;
		if(i == thread_num - 1) end[i] = N - 1;
		else end[i] = (N/thread_num) * (i+1);
	}
	omp_set_num_threads(thread_num);
	t0 = omp_get_wtime();
	//并行——小数组求前缀和,对 thread_num 个子数组进行并行求前缀和
	#pragma omp parallel shared(a,begin,end)
	{
		int nthreads = omp_get_thread_num();
		b[begin[nthreads]]=a[begin[nthreads]];
		for(int i=begin[nthreads]+1;i<=end[nthreads];i++){
			b[i]=b[i-1]+a[i];
		}
	}
	//并行——对后面的 thread_num-1 个子数组,加上前一个子数组的末尾值 
	for(int i = 1; i < thread_num; i++){
		double temp = b[end[i-1]];  
		#pragma omp parallel for shared(begin,end,i,temp) schedule(dynamic,100000)
		for(int j = begin[i]; j <= end[i]; j++){
			b[j] += temp;
		}
	}
	t1 = omp_get_wtime();
	//串行验证代码
	int flag=1;
	for(int i = 0; i < N; i++){
		if(i != 0)  a[i] += a[i-1];
		if(abs(b[i]-a[i])>1e-4){
			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协议,将本地的a.ccp的代码文件传输到“/2021150233”的目录下。然后,打开shell窗口,使用“ssh bxjs@hpc.szu.edu.cn”连接A0服务器,但是由于A1服务器只有32个核,而A0有64个核,所以下面的实验数据在A0服务器上执行获得的。

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

    ​​​​​​​

图1  b文件的执行结果

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

线程数

执行时间/s

1

2

4

8

16

32

64

第1次

0.88835

0.99933

0.87383

0.70499

0.47305

0.34751

0.29341

第2次

1.33377

1.00125

0.86595

0.69410

0.45761

0.34307

0.27853

第3次

1.46752

1.00855

0.86006

0.69366

0.48072

0.33718

0.27508

第4次

1.46913

1.02314

0.85582

0.70078

0.46284

0.33474

0.28620

第5次

1.47133

1.02513

0.89273

0.70233

0.46039

0.34785

0.29076

平均值

1.32602

1.01148

0.86964

0.69917

0.46692

0.34207

0.29076

加速比

1.00000

1.31097

1.524792

1.89656

2.83399

3.86933

4.56053

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

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

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

同时,观测到加速比不是简单随线程数的增加而线性增加的,两者不是简单的正比关系,加速比随着线程数的增加,但是增长越来越缓慢了。这很大程度上与前面代码描述部分分析的“并行完成数组a求前缀和操作,需要的时间是 次加法的时间”有很大关系,执行时间与线程数并不是简单的反比关系。并且随着线程数的增加,CPU对任务调度的开销也随之增加,导致加速效果也在一定程度上被削弱,最终导致两者呈现出这种关系。

六、实验结论

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

然而,我并没有放弃,而是积极地寻找解决方案。我开始查阅相关资料,阅读并行算法的文献和教程,深入了解了并行计算的原理和方法。通过学习和实践,我逐渐掌握了如何将前缀和的计算任务分解成多个并行子任务,并利用多核CPU资源进行加速。

在上手写代码实现的过程中,我遇到了许多挑战和问题。例如,如何合理地划分数据,如何处理并行任务之间的数据依赖关系等等。但是,每当我遇到困难时,我都会认真思考,并寻找解决办法。通过不断地尝试和调试,我逐步解决了这些问题,最终成功地实现了前缀和的并行算法。

在实验过程中,使用了OpenMP并行编程框架,充分利用了多核CPU资源,通过并行化加快了程序的执行速度;采用了合理的数据划分方法,将数组分割成多个子数组,实现了并行计算的任务分配;通过动态调整任务分配的方式(如在第二个并行域中采用了schedule(dynamic,10000)),避免了负载不均衡带来的性能损失。

从实验结果中,得知,随着线程数的增加,求解长度为100,000,000的数组的前缀和所需的时间逐渐减少,加速比逐渐增加,说明并行化有效提高了程序性能。然而,加速比并非简单地与线程数呈线性关系,而是随着线程数增加而逐渐减弱,这与任务调度的开销以及数据划分等因素有关。在实验中,对程序进行性能分析是必不可少的,通过对不同参数下程序的运行时间和加速比进行测量和分析,可以更好地理解程序的性能特征,并针对性地进行优化和改进。

总的来说,本实验通过设计并实现前缀和的并行算法,掌握了并行编程的基本方法和技术,同时通过性能分析对并行程序进行评估,深入理解了并行计算的原理和优化策略,为进一步深入学习并行计算奠定了基础。

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MPI(Message Passing Interface)和OpenMP(Open Multi-Processing)是两种常用的并行程序设计方法。 MPI是一种消息传递程序设计模型,主要用于分布式内存系统中的并行计算。在MPI编程中,程序中的各个进程通过发送和接收消息来进行通信和数据传递。每个进程都有自己的内存空间,并且可以直接访问和修改自己的内存,而不能直接访问其他进程的内存。MPI程序设计可以实现大规模的并行计算,适合于需要在多台计算机上进行并行计算的情况。 OpenMP是一种共享内存的并行程序设计模型,主要用于多核共享内存架构中的并行计算。在OpenMP编程中,程序运行在多个线程中,这些线程之间可以共享一部分或全部的内存。通过使用Pragmas和语句来指定并行区域和任务分配,开发者可以将串行程序转化为并行程序,以实现更高效的计算。OpenMP程序设计适用于多核心处理器上的并行计算,能够充分发挥多核处理器的计算能力。 MPI和OpenMP有各自适用的场景和优势。MPI适用于需要在多个计算节点之间进行通信和数据传递的并行计算,可以实现集群或分布式计算;而OpenMP适用于在同一计算节点上的多核共享内存并行计算,可以利用多核处理器的并行特性。 在一些计算任务中,可以结合使用MPI和OpenMP来充分利用多节点和多核心的并行能力。通过MPI将多个计算节点连接起来,每个节点上运行多个OpenMP线程,可以实现更大规模和更高效率的并行计算。 总之,MPI和OpenMP是两种常用的并行程序设计方法,它们分别适用于分布式内存和共享内存的并行计算。根据具体的应用场景和硬件环境,可以选择合适的并行程序设计方法来实现高效的并行计算。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值