HPC学习(二)

OMP学习

motivation:刚接触高性能运算,学习方向有点乱,偶然在网上找到一篇博客: ASC18华农队长超算竞赛完整备战指南.,决定按照这个思路进行整理学习。本篇博客主要用于本人学习记录,如有错误,欢迎各位大佬指出。(大佬勿喷…)
这篇继续按照指南顺序,学习openmp编程。

学omp推荐b站网课:新竹清华大学并行计算与并行编程课程,挑选着看,感觉不需要太多基础都可以看懂。

introduction

OpenMP = open specification for multi-processing
OpenMP是由一组计算机硬件和软件供应商联合定义的应用程序接口(API);
OpenMP为基于共享内存的并行程序的开发人员提供了一种便携式和可扩展的编程模型,其API支持各种架构上的C/C++和Fortran;
共享内存模型omp的并行模型称为fork-join模型
fork-join模型由主线程创造出多个线程(fork过程),
并行代码执行完后,只剩下一个主线程(join过程)

环境配置

  1. 在Windows下很简单,vs本身就支持omp,只需要在项目属性页上左侧选择“配置属性”——“C/C++”——“语言”,然后在右侧“OpenMP支持”后填入"Yes",然后在文件中include进<omp.h>即可。
  2. 在linux下,一般gcc跟icc都.支持openmp,只要在编译的时候加上参数即可
gcc -fopenmp source.c -o source

OpenMP API

关于OpenMp的API介绍,有一位博主已经写得很全面了,想要深入学习的话去看看。以下内容参考了下方博客并根据我自己的需要简化,会相对简略。(博客链接:OpenMP学习笔记

OpenMP API总览
编译器指令(44个)
运行时库函数(35个)
环境变量(13条个)

编译器指令

编译器指令在你的源代码中可能被显示为注释,并且被编译器所忽略,除非你明显地告诉编译器
OpenMP的编译器指令的目标主要有:

  1. 产生一个并行区域
  2. 划分线程中的代码块
  3. 在线程之间分配循环迭代
  4. 序列化代码段
  5. 同步线程间的工作
指令作用
parallel用在一个结构块之前,表示这段代码将被多个线程并行执行
for用于for循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担。注意要保证每次循环之间无数据相关性。
parallel forparallel和for指令的结合,也是用在for循环语句之前,表示for循环体的代码将被多个线程并行执行,它同时具有并行域的产生和任务分担两个功能
sections用在可被并行执行的代码段之前,用于实现多个结构块语句的任务分担,可并行执行的代码段各自用section指令标出(注意区分sections和section
parallel sectionsparallel和sections两个语句的结合,类似于parallel for
single用在并行域内,表示一段只被单个线程执行的代码
private子句可以将变量声明为线程私有,每个线程都有一个该变量的副本,不会互相影响。原变量在并行部分不起任何作用,也不会受到并行部分内部操作的影响。
firstprivate在private的基础上,用原变量的值初始化线程中的副本
lastprivate在并行部分结束时,将程序语法上最后一次迭代的值赋回给变量
threadprivate只针对全局变量,使得每个线程都有一个私有的全局对象
sharedshared可以将一个变量声明为共享变量,在多个线程中共享,但要注意使用安全。
reductionreduction子句可以对一个或者多个参数指定一个操作符,然后每一个线程都会创建这个参数的私有拷贝,在并行区域结束后,迭代运行指定的运算符,并更新原参数的值。eg:reduction(+: sum)
copyincopyin子句可以将主线程中变量的值拷贝到各个线程的私有变量中,让各个线程可以访问主线程中的变量。copyin的参数必须要被声明称threadprivate.
schedule(method=static, size=1)线程分配方式,method参数有static(平均分配,线程间迭代数最多相差size),dynamic(根据程序执行动态分配),size表示每次分配的迭代树最少是多少。

库函数

接下来介绍部分常用的库函数

库函数作用
int omp_get_num_procs(void)返回调用函数时可用的处理器数目
int omp_get_num_threads(void)返回当前并行区域中的活动线程个数
int omp_get_thread_num(void)返回当前线程号
int omp_set_num_threads(void)设置进入并行区域时,将要创建的线程个数
int omp_get_max_threads()返回最大线程数量,可用上一个函数修改
int omp_in_parallel()可以判断当前是否处于并行状态,返回0或1
void omp_set_dynamic(int)该函数可以设置是否允许在运行时动态调整并行区域的线程数。0表示禁用,其他数字表示可用。
int omp_get_dynamic()返回当前程序是否允许在运行时动态调整并行区域的线程数。

互斥锁

与互斥锁相关的函数我独立开一个表格来介绍

库函数作用
void omp_init_lock(omp_lock)初始化互斥锁
void omp_destroy_lock(omp_lock)销毁互斥锁
void omp_set_lock(omp_lock)获得互斥锁
void omp_unset_lock(omp_lock)释放互斥锁
bool omp_test_lock(omp_lock)该函数可以看作是omp_set_lock的非阻塞版本。

补充一下omp_lock变量。

定义:
omp_lock_t lock;
使用:
omp_init_lock(&lock)

也就是说,omp_lock是一个omp_lock_t类型变量的指针

数据依赖与冲突

关于这方面的内容国内网站上好像比较少也比较散乱,我就按自己的理解说一下(水平有限,不敢保证正确性…)
什么叫数据依赖(carries dependency)?举个栗子就能理解:
c = a + b;
e = c + d;
c 数据依赖于 e,因为它需要 c 的值。
(应该没理解错吧…)

数据冲突
在多线程编程中,当各个线程对某一个变量同时进行读取修改的时候,线程读取的顺序就会对结果造成影响,这时候就会发生数据冲突。(个人浅见)

原子操作与锁

原子操作理解

首先要理解什么是原子操作。下面的解释是从原子操作这篇博客中截取的。

原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
现代操作系统中,一般都提供了原子操作来实现一些同步操作,所谓原子操作,也就是一个独立而不可分割的操作。在单核环境中,一般的意义下原子操作中线程不会被切换,线程切换要么在原子操作之前,要么在原子操作完成之后。更广泛的意义下原子操作是指一系列必须整体完成的操作步骤,如果任何一步操作没有完成,那么所有完成的步骤都必须回滚,这样就可以保证要么所有操作步骤都未完成,要么所有操作步骤都被完成。
例如在单核系统里,单个的机器指令可以看成是原子操作(如果有编译器优化、乱序执行等情况除外);在多核系统中,单个的机器指令就不是原子操作,因为多核系统里是多指令流并行运行的,一个核在执行一个指令时,其他核同时执行的指令有可能操作同一块内存区域,从而出现数据竞争现象。多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。

说实话,在阅读完上述解释后,我还是有些云里雾里的,我再找了一个简单的程序,也许能帮助理解。

#include<iostream>
#include<omp.h>
#include<time.h>
#include<Windows.h>
using namespace std;

int main(int argc, char** argv)
{
        omp_set_num_threads(6);
        int counter = 0, i;
#pragma omp parallel
        {
            for (i = 0; i < 10; i++)
            {
#pragma omp atomic
                counter++;
            }
        }
        cout << "counter ="  << counter;
        return 0;
}

一共6个线程,因此理论输出为6*10=60,符合实际结果。

原子操作与锁的区别

  1. 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。
  2. 原子操作是针对某个值的单个互斥操作。
  3. 可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

一般而言,使用原子操作能得到更好的性能。

OpenMP实操(多线程矩阵乘法)

//矩阵乘法
#include<iostream>
#include<omp.h>
#include<time.h>
#include<Windows.h>
using namespace std;

//int main(int argc, char** argv)
//{
//        omp_set_num_threads(6);
//        int counter = 0, i;
//#pragma omp parallel
//        {
//            for (i = 0; i < 10; i++)
//            {
//#pragma omp atomic
//                counter++;
//            }
//        }
//        cout << "counter ="  << counter;
//        return 0;
//    
//
//}

#define Max 10
#define Min -10
#define Size 500

//随机数生成
double getrandom(int max = Max, int min = Min)
{
	double tmp1 = double(rand() % 101) / 101;
	double tmp2 = (double)((rand() % (max - min)) + min);
	return tmp1+tmp2;
}

//生成指定大小的随机方阵
void getmatrix(double* Matrix,int size = Size)
{
	for (int i = 0; i < size; i++)
		for (int j = 0; j < size; j++)
			Matrix[i * size + j] = getrandom();
}

//微秒级别的windows平台计时函数
//

//传统方法
double* baseline(double* A, double* B, int size = Size)
{
	double* result;
	result = new double[size * size];
	for(int i = 0; i < size; i++)
		for (int j = 0; j < size; j++) {
			result[i * size + j] = 0;
			for (int k = 0; k < size; k++)
				result[i * size + j] += A[i * size + k] * B[k * size + j];
		}
	return result;
}

//多线程版本1
double* Multithread(double* A, double* B, int size = Size)
{
	double* result;
	result = new double[size * size];
	int i, j, k;

#pragma omp parallel for schedule(dynamic) firstprivate(i,j,k)
	for (i = 0; i < size; i++)
//#pragma omp for
		for (j = 0; j < size; j++) {
			result[i * size + j] = 0;
//#pragma omp parallel for num_threads(3)
			for (k = 0; k < size; k++)
				result[i * size + j] += A[i * size + k] * B[k * size + j];
		}
	return result;
}

int main(int argc, char** argv)
{
	double* A, * B, * C;
	int n = Size;
	srand((unsigned)time(NULL));

	//分配动态空间
	A = new double[n * n];
	B = new double[n * n];
	C = new double[n * n];

	getmatrix(A);
	getmatrix(B);

	//初始化结束,准备开始计时
	LARGE_INTEGER  large_interger;    //记录频率和周期的结构体
	double dff;       //频率
	__int64  c1, c2; //周期
	QueryPerformanceFrequency(&large_interger);
	dff = large_interger.QuadPart;
	QueryPerformanceCounter(&large_interger);
	c1 = large_interger.QuadPart;

	C = baseline(A, B);

	QueryPerformanceCounter(&large_interger);
	c2 = large_interger.QuadPart;
	cout << "baseline运行时间为(单位:毫秒):" << (c2 - c1) * 1000 / dff << '\n';
	cout << C[50, 50] << '\n';

	QueryPerformanceFrequency(&large_interger);
	dff = large_interger.QuadPart;
	QueryPerformanceCounter(&large_interger);
	c1 = large_interger.QuadPart;

	C = Multithread(A, B);

	QueryPerformanceCounter(&large_interger);
	c2 = large_interger.QuadPart;
	cout << "多线程矩阵乘法1运行时间为(单位:毫秒):" << (c2 - c1) * 1000 / dff << '\n';
	cout << C[50, 50] << '\n';

	delete [] A;
	delete [] B;
	delete [] C;

	return 0;
}

代码部分会有几个警告,还不太清楚要如何处理,但似乎对结果没有什么影响。
事实上,通过修改循环顺序以及使用矩阵分块能够进一步提升代码性能,但我是懒得写了,,,有兴趣的可以参考“程序性能优化探讨(6)——矩阵乘法优化之分块矩阵”,这篇写的非常详细。


openmp暂时就写那么多,以后有想法会继续补充。
下一篇应该会开始写MPI。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值