寻找完数的OpenMP并行程序

深圳大学  并行计算  实验二

一、实验目的

1. 掌握for编译制导语句和schedule子句;

2. 理解数据竞争,掌握同步结构;

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

二、实验环境

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

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

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语言编写程序,求小于等于n的所有完数(一个数恰好等于它的因子之和),并存放在数组a中,调节for编译制导语句中schedule的参数,使得执行时间最短。为了验证结果正确性,将并行计算结果和串行计算结果相比较。

代码实现思路:

首先,考虑如何判断一个数是否为完数,这里考虑采用试除法,求出该数的所有因数进行相加然后减去这个数,来获得该数的因子之和,然后与这个数本身进行比较,相等即为完数,反之久不是完数。在下面的初步代码实现中,将该判断方法封装成了bool wanshu(int t)函数,方便需要时直接调用。

接着,我们考虑用构建int类型的一维数组a、b,分布用于存储并行计算和串行计算的结果。接着,我们考虑对a、b进行随机的初始化,这里为了避免复杂的赋值初始化操作,可以直接为两个数组a、b加上两个数组指针cnt_a、cnt_b,通过将cnt_a、cnt_b设置为0,实现对两个数组的初始化。

       然后,我考虑使用了OpenMP库中,omp_set_num_threads(int tp)函数用于设置并行区域的线程数为tp。再omp_get_wtime()函数,获取当前线程距离线程线程开始的时间差,赋值给double类型的变量t0,作为并行域计算的起始时间。接着,使用“#pragma omp parallel for shared(a,cnt_a,i) schedule(dynamic,10000)”编译指导语句,构建一个for循环并行域,然后在并行域中实现一个一层for循环,其中调用wanshu(int i)函数,实现对1n是否为完数进行判断,并将结果通过“a[cnt_a++]=i”该行代码,存储到a数组中。但是由于该行代码不是原子操作,所以使用“#pragma omp critical”编译指导语句,保证该行代码在同一时刻只能由一个线程执行。

接着,再次使用omp_get_wtime()函数,获取当前线程距离线程线程开始的时间差,赋值给double类型的变量t1,作为并行域计算的结束时间。继而,通过t1-t0就可以获得该并行域的运行时间。紧接着,再通过一个一层for循环,串行实现求出1n的所有完数,并将结果存储到b数组中。

       最后,由于不能保证a数组中,多线程写入完数的顺序,与串行的执行的按从小到大写入的一致,所以采用两层for循环,保证a数组中数一定在b数组中出,并且保证a数组和b数组的大小相对,从而保证并行结果和串行结果的一致性,即可认为并行计算是正确的。

初步代码实现:

//判断一个数是否为完数,是返回true,不是返回false 
bool wanshu(int t){
	if(t==1) return false;
	else{
		int sum=-t;
		for(int i=1;i*i<=t;i++){
			if(t%i==0)
				if(i*i==t) sum+=i;
				else sum+=i+(t/i);
			if(sum>t) return false;
		} 
		return sum==t;
	}
} 
//初始化参数 
omp_set_num_threads(thread_num);
int cnt_a=0,cnt_b=0,i,j;
// 并行代码
double t0 = omp_get_wtime();
#pragma omp parallel for shared(a,cnt_a,i) schedule(dynamic,10000)
for(i=1;i<N;i++)
	if(wanshu(i)){
		#pragma omp critical
		a[cnt_a++]=i;
	}
double t1 = omp_get_wtime();
//串行代码 
for(i=1;i<=N;i++)
	if(wanshu(i)) b[cnt_b++]=i;
//正确性的验证 
if(cnt_a!=cnt_b) cout << "Results are not equal!" << endl;
else
	for(i=0;i<cnt_a;i++){
		bool flag=true;
		for(j=0;j<cnt_b;j++){
			if(a[i]==b[i]) {
				flag=false;
				break;	
			}
		}
		if(flag) cout << "Results are not equal!" << endl;
	}
cout << "Results are equal!" << endl;
cout << "Thread num: "<<thread_num<<", Parallel time: " << t1 - t0 <<"second"<< endl;

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

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

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

四、代码描述

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

在double achieve(int thread_num)函数中,主要通过“#pragma omp parallel for shared(a,cnt_a,i) schedule(dynamic,10000)”和“#pragma omp critical”编译制导语句实现并行域,然后将结果与串行执行结果进行比较,保证结果的准确性。而其中最频繁调用的判断一个数是否为完数的函数bool wanshu(int t),是通过试除法实现的,其时间复杂度为O( )。

#include <omp.h>
#include <bits/stdc++.h>
using namespace std;
const int N=5000000;//求解范围 
const int ThreadNum_CanShu=7;//线程数组的大小
const int set_thread_num[]={1,2,4,8,16,32,64};//线程数
const int XunHuan_CanShu=5;//循环次数 
int a[1000],b[1000];//a为并行结果、b为串行验证结果 
//判断一个数是否为完数,是返回true,不是返回false 
bool wanshu(int t){
	if(t==1) return false;
	else{
		int sum=-t;
		for(int i=1;i*i<=t;i++){
			if(t%i==0)
				if(i*i==t) sum+=i;
				else sum+=i+(t/i);
			if(sum>t) return false;
		} 
		return sum==t;
	}
} 
//根据传入的线程数,进行函数的调用 
double achieve(int thread_num){
	//初始化参数 
	omp_set_num_threads(thread_num);
	int cnt_a=0,cnt_b=0,i,j;
	// 并行代码
	double t0 = omp_get_wtime();
       #pragma omp parallel for shared(a,cnt_a,i) schedule(dynamic,10000)
	for(i=1;i<N;i++)
		if(wanshu(i)){
			#pragma omp critical
			a[cnt_a++]=i;
		}
	double t1 = omp_get_wtime();
	//串行代码 
	for(i=1;i<=N;i++)
		if(wanshu(i)) b[cnt_b++]=i;
	//正确性的验证 
	if(cnt_a!=cnt_b) cout << "Results are not equal!" << endl;
	else
		for(i=0;i<cnt_a;i++){
			bool flag=true;
			for(j=0;j<cnt_b;j++){
				if(a[i]==b[i]) {
					flag=false;
					break;	
				}
			}
			if(flag) cout << "Results are not equal!" << endl;
		}
	cout << "Results are equal!" << endl;
	cout << "Thread num: "<<thread_num<<", 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<<"秒"<<endl;;
		cout<<"----------------------------------"<<endl; 
	} 
	return 0;
}

五、实验结果和分析

通过在文件目录下输入“ftp://hpc.szu.edu.cn”,将本地的b.cc的代码文件赋值到“/2021150233”的目录下,通过“g++ -o b b.cc -fopenmp”将b.cc文件编译为b文件。然后通过“./b”运行b文件,最终执行的结果如下图1所示,并将执行的结果记录进表1中,同时计算出加速比(与线程数=1时的执行时间相比)

   

图1  b文件的执行结果

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

线程数

执行时间/s

1

2

4

8

16

32

64

第1次

94.8152

58.9962

29.5812

14.4250

8.5109

4.1468

2.1349

第2次

115.7560

57.4844

29.6176

16.0980

8.3331

4.2075

1.9023

第3次

109.4420

61.6450

32.3688

16.8249

8.0214

3.6610

1.9320

第4次

113.7070

60.2969

28.4308

15.7933

7.8449

4.0075

2.2150

第5次

113.5680

58.4268

30.5794

14.3456

8.0058

3.8983

2.0441

平均值

109.4580

59.3698

30.1156

15.4974

8.1432

3.9842

2.0456

加速比

1.0000

1.8437

3.6346

7.0630

13.4416

27.4730

53.5090

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

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

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

同时,观测到加速比与线程数存在线性关系,但是并不是预期的“加速比==线程数”的关系,而是“加速比==a*线程数(0<a<1)”的关系。这主要是由于随着线程数的增加,CPU对任务调度的开销也随之增加,导致出现“加速比==a*线程数(0<a<1)”的关系。

六、实验结论

通过实验我们探索了在不同线程数下并行程序的执行时间和加速比,并得出以下结论:

  1. 效率提升与线程数增加呈现非线性关系: 随着线程数的增加,执行时间逐渐减少,加速比逐渐上升。然而,加速比与线程数并不是简单的线性关系,而是在一定范围内呈现出“加速比=a*线程数”的关系,其中a为一个小于1的系数。这是因为随着线程数增加,CPU对任务调度的开销也增加,从而导致了效率提升的非线性关系。
  2. 并行程序的正确性验证至关重要: 在并行计算中,尤其是涉及到多线程操作时,保证程序的正确性显得尤为重要。在实验中,我们通过对并行计算结果与串行计算结果的比较,以及对完数的验证,确保了并行计算的准确性。
  3. 并行计算中的同步机制: 在并行计算中,我们使用了OpenMP的omp critical指令来保证多线程写入结果数组时的同步,以避免数据竞争导致的错误结果。
  4. 动态调度的优势: 实验中我们使用了schedule(dynamic,10000)来动态调度任务,根据任务的大小动态分配给各个线程,这样可以更加灵活地利用系统资源,提高了并行计算的效率。
  5. 性能分析的重要性: 在实验中,我们进行了简单的性能分析,通过对不同线程数下的执行时间和加速比进行对比分析,从中得出了并行计算的优化效果。这种性能分析对于优化算法和程序设计至关重要。

总的来说,通过这次实验,我们深入理解了并行计算的基本原理和技术方法,掌握了一些OpenMP的使用方法,并通过实验获得了关于并行计算性能和效率的一些实用经验。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值