深圳大学 并行计算 实验二
一、实验目的
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)函数,实现对1到n是否为完数进行判断,并将结果通过“a[cnt_a++]=i”该行代码,存储到a数组中。但是由于该行代码不是原子操作,所以使用“#pragma omp critical”编译指导语句,保证该行代码在同一时刻只能由一个线程执行。
接着,再次使用omp_get_wtime()函数,获取当前线程距离线程线程开始的时间差,赋值给double类型的变量t1,作为并行域计算的结束时间。继而,通过t1-t0就可以获得该并行域的运行时间。紧接着,再通过一个一层for循环,串行实现求出1到n的所有完数,并将结果存储到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)”的关系。
六、实验结论
通过实验我们探索了在不同线程数下并行程序的执行时间和加速比,并得出以下结论:
- 效率提升与线程数增加呈现非线性关系: 随着线程数的增加,执行时间逐渐减少,加速比逐渐上升。然而,加速比与线程数并不是简单的线性关系,而是在一定范围内呈现出“加速比=a*线程数”的关系,其中a为一个小于1的系数。这是因为随着线程数增加,CPU对任务调度的开销也增加,从而导致了效率提升的非线性关系。
- 并行程序的正确性验证至关重要: 在并行计算中,尤其是涉及到多线程操作时,保证程序的正确性显得尤为重要。在实验中,我们通过对并行计算结果与串行计算结果的比较,以及对完数的验证,确保了并行计算的准确性。
- 并行计算中的同步机制: 在并行计算中,我们使用了OpenMP的omp critical指令来保证多线程写入结果数组时的同步,以避免数据竞争导致的错误结果。
- 动态调度的优势: 实验中我们使用了schedule(dynamic,10000)来动态调度任务,根据任务的大小动态分配给各个线程,这样可以更加灵活地利用系统资源,提高了并行计算的效率。
- 性能分析的重要性: 在实验中,我们进行了简单的性能分析,通过对不同线程数下的执行时间和加速比进行对比分析,从中得出了并行计算的优化效果。这种性能分析对于优化算法和程序设计至关重要。
总的来说,通过这次实验,我们深入理解了并行计算的基本原理和技术方法,掌握了一些OpenMP的使用方法,并通过实验获得了关于并行计算性能和效率的一些实用经验。