简单排序的OpenMP并行程序

深圳大学  并行计算  实验三

一、实验目的

1. 实现简单排序的OpenMP并行程序;

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

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:先将数组a划分成p个子数组,每个线程排序1个子数组,然后将这p个子数组归并到数组a或另一个数组b中,其中归并步骤可以串行。为了验证结果正确性,将并行计算结果和串行计算结果相比较。

代码实现思路:

首先,我们考虑构建两个int类型的一维数组a、b,数组a用来存储并行排序的结果,数组b用于存储串行排序的结果。接着,我们考虑对a、b进行随机的初始化。每排序前,对a、b进行随机初始化,才能避免分配的内存中的值对实验产生影响,更好地验证并行化结果的正确性。对此,我采用了cstdlib库中rand()函数获取一个0到RAND_MAX之间的随机整数,并将该随机浮点数同时赋值给数组a、数组b。

接着,考虑如何实现并行的排序。这里我先定义了两个长度为thread_num的一维数组begin和end,然后采用for循环将长度为n的数组a,分成thread_num个子数组。具体就是通过用数组begin和end存储子数组的起始和结束的下标来实现的。

       然后,我考虑使用了OpenMP库中,omp_set_num_threads(int thread_num)函数用于设置并行区域的线程数为tp。再omp_get_wtime()函数,获取当前线程距离线程线程开始的时间差,赋值给double类型的变量t0,作为并行域计算的起始时间。接着,使用“#pragma omp parallel shared(a,begin,end)”编译指导语句,构建一个并行域,然后在并行域中,每个线程通过omp_get_thread_num()函数获取该线程的线号nthreads,然后通过线程号nthreads获取该线程具体需要排序的子数组的起始begin[nthreads]和结束end[nthreads]下标,然后通过调用sort(a+begin[nthreads],a+end[nthreads]+1)函数对子数组进程排序,实现来多个子数组的并行排序。

在完成了thread_num个子数组的并行排序后,考虑对这个thread_num个子数组进行归并,为此,我专门实现了void guibing(int begin1, int end1, int begin2, int end2,int*shuzu)函数,实现对shuzu数组的两个连续的[begin[j], end[j]]和[begin[j+i], end[j+i]]的元素进行归并(前闭后闭区间),供给调用。

然而,同时考虑到如何直接串行对这thread_num个子数组进行串行排序的时间复杂度可可能比较高,所以考虑采用并行的方式,对这thread_num个子数组进行归并。这里在实现并行的时候,最主要是需要控制好归并的顺序,不然会导致结果的失败。

具体,我采用了两个for循环来实现,最外层for循环用于控制归并的步长,确定该次执行的需要执行(thread_num/i+1)/2归并。然后使用“#pragma omp parallel for shared(a,begin,end,i)”编译指导语句,构建一个for循环并行域,在其中并行的执行(thread_num/i+1)/2个归并操作。

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

       最后,通过执行一个for循环实现对a数组和b数组的比较,来保证并行排序结果的正确性。

初步代码实现:

//对shuzu数组的两个连续的[begin[j], end[j]]和[begin[j+i], end[j+i]]的元素进行归并,前闭后闭区间 
void guibing(int begin1, int end1, int begin2, int end2,int*shuzu){
	int *tempkfc;
	tempkfc=new int[end2-begin1+1];
	int cnt_temp=0,cnt1=begin1,cnt2=begin2;
	while(cnt1<=end1||cnt2<=end2){
		if(cnt1<=end1&&cnt2<=end2)
			if(shuzu[cnt1]<shuzu[cnt2]) tempkfc[cnt_temp++]=shuzu[cnt1++];
			else tempkfc[cnt_temp++]=shuzu[cnt2++];
		else if(cnt1<=end1) tempkfc[cnt_temp++]=shuzu[cnt1++];
		else if(cnt2<=end2) tempkfc[cnt_temp++]=shuzu[cnt2++];
	}
	for(int i=0;i<cnt_temp;i++) shuzu[i+begin1]=tempkfc[i];
	delete[]tempkfc;
}
int a[N],b[N];//a为并行结果、b为串行验证结果 
//初始化参数 
srand(time(NULL));
for (int i = 0; i < N; i++)
	a[i] = b[i] = rand();//0 ~ (RAND_MAX)32767
double t0,t1;
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();
	sort(a+begin[nthreads],a+end[nthreads]+1); 
}
//并行——优化后的归并,对 thread_num 个子数组进行并行归并 
for(int i=1;i<thread_num;i*=2){
	num=thread_num/i;
	num=(num+1)/2;
	#pragma omp parallel for shared(a,begin,end,i)
	for(int j=0;j<num;j++){
		guibing(begin[j*(2*i)], end[j*(2*i)], begin[j*(2*i)+i], end[j*(2*i)+i], a);
		end[j*(2*i)]=end[j*(2*i)+i];
	}
}
t1 = omp_get_wtime();
//串行排序代码 
sort(b,b+N);
//验证代码
int flag=1;
for(int i = 0; i < N; i++)
	if(b[i]!=a[i]||(i>0 && a[i] < a[i-1])){
		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 achieve(int thread_num)”中,获取运行时间,并计算出对应5次实验的平均值。从而,避免了程序的多次间断的运行带来的误差。具体的代码在下列“四、代码描述”中可见。

四、代码描述

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

在double achieve(int thread_num)函数中,主要通过“#pragma omp parallel shared(a,begin,end)”和“#pragma omp parallel for shared(a,begin,end,i)”编译制导语句实现并行域,然后将结果与串行执行结果进行比较,保证结果的准确性。

而其中,并行排序中,一开始对thread_num个子数组进行分组和并行排序的理论时间复杂度为 ,子数组归一化的理论时间复杂度为  。相比于串行排序,可以有一个常数基级别的优化。

#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;//循环次数 
int a[N],b[N];//a为并行结果、b为串行验证结果 
//对shuzu数组的两个连续的[begin[j], end[j]]和[begin[j+i], end[j+i]]的元素进行归并,前闭后闭区间 
void guibing(int begin1, int end1, int begin2, int end2,int*shuzu){
	int *tempkfc;
	tempkfc=new int[end2-begin1+1];
	int cnt_temp=0,cnt1=begin1,cnt2=begin2;
	while(cnt1<=end1||cnt2<=end2){
		if(cnt1<=end1&&cnt2<=end2)
			if(shuzu[cnt1]<shuzu[cnt2]) 
				tempkfc[cnt_temp++]=shuzu[cnt1++];
			else tempkfc[cnt_temp++]=shuzu[cnt2++];
		else if(cnt1<=end1) tempkfc[cnt_temp++]=shuzu[cnt1++];
		else if(cnt2<=end2) tempkfc[cnt_temp++]=shuzu[cnt2++];
	}
	for(int i=0;i<cnt_temp;i++)
		shuzu[i+begin1]=tempkfc[i];
	delete[]tempkfc;
}
//根据传入的线程数,进行函数的调用 
double achieve(int thread_num){
	//初始化参数 
	srand(time(NULL));
	for (int i = 0; i < N; i++) a[i] = b[i] = rand();//0 ~ (RAND_MAX)32767
	double t0,t1;
	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();
		sort(a+begin[nthreads],a+end[nthreads]+1); 
	}
	//并行——优化后的归并,对 thread_num 个子数组进行并行归并 
	for(int i=1;i<thread_num;i*=2){
		num=thread_num/i;
		num=(num+1)/2;
		#pragma omp parallel for shared(a,begin,end,i)
		for(int j=0;j<num;j++){
			guibing(begin[j*(2*i)], end[j*(2*i)], 
				begin[j*(2*i)+i], end[j*(2*i)+i], a);
			end[j*(2*i)]=end[j*(2*i)+i];
		}
	}
	t1 = omp_get_wtime();
	//串行排序代码 
	sort(b,b+N);
	//验证代码
	int flag=1;
	for(int i = 0; i < N; i++)
		if(b[i]!=a[i]||(i>0 && a[i] < a[i-1])){
			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;
	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”,将本地的c.pp的代码文件赋值到“/2021150233”的目录下,连接A1机器,通过“g++ -o a.out a.cpp -fopenmp -O3”将a.cpp文件编译为c.out文件。然后通过“./c.out”运行c.out文件,最终执行的结果如下图1所示,并将执行的结果记录进表1中,同时计算出加速比(与线程数=1时的执行时间相比)

 

图1  c.out文件的执行结果

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

线程数

执行时间/s

1

2

4

8

16

32

64

第1次

11.3753

6.5231

4.3566

3.5897

3.1774

3.3442

3.2583

第2次

11.0783

6.5305

4.3493

3.6416

3.1178

3.0552

3.2258

第3次

11.0455

6.5340

4.3620

3.5750

3.1662

3.1977

3.3308

第4次

11.0799

6.5353

4.3518

3.8005

3.1833

3.1257

3.2058

第5次

11.0661

6.4750

4.3491

3.6747

3.3242

3.5033

3.2096

平均值

11.1290

6.5196

4.3538

3.6563

3.1938

3.2452

3.2461

加速比

1.0000

1.7070

2.5562

3.0438

3.4846

3.4294

3.4284

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

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

根据对上述,表1和图2的分析可知,在一开始随着线程数的增加,排序长度为100000000所有的需要的时间逐渐下降,加速比逐渐上升,但是随着线程增加,运行时间和加数比逐渐趋于平缓,甚至运行时间有上升趋势,加速比有下降趋势。

这是因为通过增加线程数,可以使得子数组的排序任务加速,但是这个加速只是常数级别的加速。同时,随着线程数增加,又会导致子数组的归并开销加大,线程调度的开销加大,所以当线程数增加到一定程度时,通过增加线程数的排序加速会逐渐消失,使得最后的运行时间和加数比逐渐趋于平缓。

六、实验结论

根据实验结果和分析,可以得出以下结论:

  1. 随着线程数增加,排序时间逐渐减少,然后趋于平缓甚至有增加趋势:在一开始,增加线程数可以有效地加快排序任务的执行,从而降低排序时间,同时加速比也随之增加。然而,随着线程数进一步增加,线程间的调度开销和子数组归并的开销开始占据主导地位,导致增加线程数对整体性能的提升逐渐减弱,甚至出现运行时间增加和加速比下降的情况。
  2. 存在线程数与性能的最优匹配点:实验结果表明,并不是线程数越多越好,存在一个最优的线程数,能够获得最佳的性能提升。超过这个最优线程数后,由于额外的线程调度开销和归并开销增加,性能提升开始减弱甚至逆转。
  3. 并行程序正确性的验证:在并行程序中,必须确保并行排序的结果与串行排序的结果一致,以验证并行算法的正确性。通过对比并行排序结果和串行排序结果,可以检查算法是否存在错误。
  4. 运用 OpenMP 并行编程技术:实验中使用了 OpenMP 库来实现并行程序,充分利用了多核 CPU 的计算资源,提高了程序的执行效率。通过设置并行域和编译制导语句,实现了对子数组的并行排序和归并操作。
  5. 实验性能的改进与优化:在实验过程中,可以考虑使用更高效的排序算法,如快速排序等,并结合更优的归并策略来进一步提高程序性能。此外,还可以考虑优化并行算法的实现,减少不必要的线程同步和通信开销,以提高并行程序的性能。

综上所述,通过本次实验,我们深入了解了并行排序算法的设计和实现过程,掌握了并行程序的开发技术和性能分析方法,同时在实验过程,通过查找性能瓶颈,不断优化程序运行时间,为优化并行程序提供了参考和思路。

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
删除加密文件 remove(encryptedFile.c_str()); // 检查配置文件是否已经过期 if (checkExpiration(configFile)) { cerr << "Trial period has expired." << endl; flock(lockFile, LOCK_UN); close(lock好的,下面是OpenMP并行奇偶排序的程序: ```c #include <stdio.h> #include <omp.h> File); return false; } // 获取当前时间 time_t now = time(NULL); // 计算截止void odd_even_sort(int *arr, int n) { #pragma omp parallel { int phase, i, tmp; for (phase = 0; phase < n; phase++) { if (phase % 2 == 0) { #pragma omp for for (i = 1; i < n; i += 2) { if (arr[i - 1日期 struct tm expDateTm; localtime_r(&now, &expDateTm); expDateTm.tm_mday += TRIAL_PERIOD; mktime(&expDateTm); char expDateStr[11]; strftime(expDateStr, sizeof] > arr[i]) { tmp = arr[i - 1]; arr[i - 1] = arr[i]; arr[i(expDateStr), "%Y-%m-%d", &expDateTm); // 写入截止日期到配置文件 if (!writeExpiration(configFile, expDateStr)) { cerr << "Failed to write expiration date to config file." << endl; ] = tmp; } } } else { #pragma omp for for (i = 1; i < n flock(lockFile, LOCK_UN); close(lockFile); return false; } // 解锁配置文件 flock - 1; i += 2) { if (arr[i] > arr[i + 1]) { tmp = arr[i(lockFile, LOCK_UN); close(lockFile); return true; } int main(int argc, char** argv) { // + 1]; arr[i + 1] = arr[i]; arr[i] = tmp; } } } } } } int main() { int arr[] = {4, 2, 5, 1, 3}; 生成随机密码 string password = generateRandomString(KEY_SIZE/8); // 限制试用期 if (! int n = sizeof(arr) / sizeof(int); odd_even_sort(arr, n); int i; for (i = limitTrialPeriod(CONFIG_FILE, password)) { return 1; } // 正常运行 cout << "Hello,0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; world!" << endl; return 0; } ``` 这个示例代码使用AES对称加密算法来加密} ``` 在这个程序中,我们使用了OpenMP的并行for指令和并行section指令,并使用奇和解密配置文件,加密后的文件保存在指定的文件中,解密后的文件会自动删除。偶排序算法进行排序。在奇数和偶数阶段分别进行排序,并使用OpenMP的并行for指令在程序运行期间,使用文件锁来锁定配置文件,以防止其他程序修改或删除该文件。程序会将每个循环并行化,使得每个线程处理自己负责的部分数据。 编译和运在启动时检查配置文件的截止日期,并在每次读取和修改配置文件时检查试用期是否行代码的命令如下: ```bash gcc -fopenmp odd_even_sort.c -o odd_even_sort export OMP已经过期,如果过期则返回0。在试用期结束后,程序会自动关闭。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值