ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

openmp理解与实践--常见问题解决

2020-12-31 18:31:01  阅读:202  来源: 互联网

标签:常见问题 -- 并行 long omp 线程 openmp include


openmp理解与实践–常见问题解决

缘起

并行计算已经有些年没有碰过了。以前做CFD时,利用网格的分块考虑任务并行,所以主要利用mpi实现。
其实在cfd中mpi并行的确是比较适合,各个进程间维护自己的内存空间,利用有限的通信来实现交互,这是一种能够很自然理解的模型。

最近实现一个扑克赢率计算的小工具,在6人情况下,5个对手,当给出公共牌时,如果采用枚举遍历计算,
需要循环:
Binomial(45,2)*Binomial(43,2)*Binomial(41,2)*Binomial(39,2)*Binomial(37,2)
这样一个实现串型程序可能需要计算好几天,于是在想能否利用并行来进行加速。然而由于这个任务中的循环是强耦合在一起的,很难采用某种方式进行任务划分,所以利用mpi来实现多个进程的并行计算可能并不是非常合适,于是想能不能利用其它方式来实现,于是又想起openmp来,尽管我以前没有用过,但还有点映象,所以学习着来用。

显然openmp对于一定编程基础的人来说,似乎实现起来应该是很简单的事情。
我也是这么自认为算是能编程的,所以在csdn上收了几个帖子来看,的确不是非常复杂的。
做个简单例子,加上头文件,加上线程并行编译标识语句,编译运行,的确ok了。
但搬到赢率计算程序上,麻烦就来了,计算结果跟串行的不一致。怎么办?
我还是根据当前的理解快点解决掉问题,再看多几个帖子,看看别人的介绍和示例,然后再去尝试,
也用各种简单示例来实践,搞了将近一天还是存在问题。我想还是不能太偷懒,有时真的匀速则不达,还是老老实实去openmp官网,进行系统学习一下吧,不然靠示例是很难解决问题的,而且有的示例的实现与其解释是不对应的,有些概念的解释也是含混不清的。显然有些帖子是没有深入的理解openmp,也没有说到点上,这些就不谈了。

我的理解

我相信问题的关键在于自身对于openmp的理解,所以我在官网上下来几个tutorial来看,的确在循序渐进的讲解下,我应该是悟了。
不讲openmp的深层次原理,我总结openmp的理解如下:

  1. 利用openmp并行是线程的并行(线程包含在进程内,一个进程可以有多个进程。多个进程的并行可以把cpu的多个核用起来,多个线程的并行也可以
    将cpu的多个核用起来)

  2. openmp是共享内存的,也就是说没有特别指定为线程私有的,所有的程序的东西都是共享的(而基于mpi的多进程并行,则是进程私有的,各个进程间不共享数据空间)。(这一点是解决问题的关键,特别是使用数组遇到一些稀奇的事情时,比如增加一句输出导致结果正确,而注释掉后结果由出错了。这个方面的错误大多与数组内存使用相关,在c、c++中常见,fortran也有。)

  3. 从“#pragma omp parallel” 开始程序进入并行区域,并行区域如果用{}包围,那么并行的代码就是包含在其中的内容,如果使用“#pragma omp parallel for”只针对for循环,那么并行的区域仅包含仅跟该标记的for循环的内部代码。在并行代码中如果使用“#pragma omp single”标记其后的一行代码或者用{}包围的代码,则表示这些代码仅在一个线程中运行,这个线程不一定是主线程。

  4. 为了程序运行结果正确,尽量减少各个线程运行的代码中的共享变量更新。可以使用private指令让各进程维护一个副本变量,但最好的方式还是使用临时变量而不要使用全局的共享变量,因为使用private维护变量副本时,有时你以为维护了,但其实没有,比如数组就容易出现这种问题,一般的变量可能在进入线程会设置成空,但数组不是自动将所有索引下的内存空间都处理的,所以如果不在线程内做赋值,很有可能会出错。

  5. 如果不可避免的要在各个线程内对全局的共享变量进行更新,那么要使用openmp提供的机制来进行处理,比如利用Reduction(规约)机制,利用critical机制,利用 atomic 机制。其中:
    Reduction机制会在各个线程中创建规约列表中的变量副本,并使用规约列表中的运算符进行局部更新,最后将各局部副本合并到全局的共享变量中。
    critical机制是利用一种互斥机制,同一时间只能在一个线程访问,称为临界区。 (Mutual exclusion: Only one thread at a time can enter a critical region.)。
    atomic 机制也是利用一种互斥机制,同一时间只能一个线程访问,但只针对一个内存地址的更新,比如“X += tmp”。(Atomic provides mutual exclusion but only applies to the update of a memory location)

  6. 数组是可以用private标记成副本的,但如前所述,在private中只记录了数组名,数组的大小信息其实线程是不清楚的,所以在线程执行代码中要进行赋值。

  7. 嵌套的在内部的循环一般不会自动并行化,而会采用串行运行。出发使用nested命令,我没有深入尝试,如有需要可以去了解。

  8. openmp还有很多更复杂的机制,但我暂时用不到了,所以也没有深入去学习,有需要的可以去了解。

实践与代码讲解

先看一个简单的串行代码,我们主要是要对其中的三重循环进行并行优化,注意到最内层的循环与其他层是没有关系的,而第二层的遍历遍历k是与第一层的遍历变量j相关的。其计算输出的sumc结果为55。

#include <iostream> 
#include <vector> 
#include <string> 
#include <time.h>

int main(int argc, const char * argv[]) { 
	clock_t start, end;
	double cpu_time_used;

	start = clock();
	
	long long suma=0;
	long long sumc=0;
	for(long long j=0;j<7;j++)
	{
		long long sumb=0;
		for(long long k=j+1;k<6;k++)
		{
			for(long long i=0;i<10000000;i++)
			{
				suma+=i;
			}
			sumb+=k;
		}
		sumc+=sumb;
	}
	
	end = clock();
	std::cout<<"sumc="<<sumc << std::endl; 
	cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
	std::cout<<"time="<<cpu_time_used << std::endl; 


    std::cout << std::endl; 

    return 0; 
}

要进行并行化,最简单的方式是采用“#pragma omp parallel for”标记,并使用规约来进行全局变量的更新,比如:
注意其中的两处修改:

  1. #include <omp.h>
  2. #pragma omp parallel for reduction(+:sumc)
    具体的作用前面已经解释过。

由于采用g++编译,所以命令加上选项 -fopenmp,比如:
g++ test-p.cpp -o testpa5.exe -fopenmp -O3

在使用g++编译情况下,这一并行的代码比前面的串行代码运行还要慢一些,如果使用编译优化选项-O3,可能差异更明显可见,所以有时候并行并非一定会带来效率的提升,要看具体的问题来处理。

#include <iostream> 
#include <vector> 
#include <string> 
#include <time.h>
#include <omp.h>

int main(int argc, const char * argv[]) { 

	clock_t start, end;
	double cpu_time_used;

	start = clock();
	long long suma=0;
	long long sumc=0;

	#pragma omp parallel for reduction(+:sumc) //这句使用了规约操作没有问题,而且加速了很多
	for(long long j=0;j<7;j++)
	{	
		//printf("j=%d id=%d\n",j,omp_get_thread_num());
		long long sumb=0;
		for(long long k=j+1;k<6;k++)
		{
			for(long long i=0;i<200000000;i++)
			{
				suma+=i;
			}
			sumb+=k;
		}
		sumc+=sumb;
		//printf("sumc=%d thread id=%d %d\n",sumc,omp_get_thread_num(),j);
	}

	end = clock();
	std::cout<<"sumc="<<sumc << std::endl; 
	cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
	std::cout<<"time="<<cpu_time_used << std::endl; 


    std::cout << std::endl; 

    return 0; 
}

第二种方式是不使用自动的并行,而自己来设计并行,如下面的代码所示
注意其中的修改:

  1. #define MAX_THREADS 4 主动定义线程的总数
  2. omp_set_num_threads(MAX_THREADS); 主动设定线程组,使并行区域根据这个设定运行,就是在未修改线程组前,后面的并行区域都按照最大线程数MAX_THREADS进行运行。
  3. #pragma omp parallel private(j,tid) 设置并行区域,后面跟着代码进入并行,并使用private指定了各线程需要维护副本的变量j,tid。
    其实更好的处理是不使用全局的j和tid,而使用临时的j和tid,即在并行区域代码中定义这两个变量。
  4. tid = omp_get_thread_num() 各线程将其id赋值给tid,用于后面的并行任务分配。
  5. for(j=tid;j<7;j+=MAX_THREADS) 设计分配各个线程运行的代码,显然因为这样的设定,各个线程运行的任务就给定了。
    对于线程0,那么运行的j循环包含j=0,j=4。
    对于线程1,那么运行的j循环包含j=1,j=5。
    对于线程2,那么运行的j循环包含j=2,j=6。
    对于线程3,那么运行的j循环包含j=3。
  6. 全局更新的变量sumc,在不使用 #pragma omp critical#pragma omp atomic时也没有出错,但最好还是用一下。
#include <iostream> 
#include <vector> 
#include <string> 
#include <time.h>
#include <omp.h>

int main(int argc, const char * argv[]) { 
	clock_t start, end;
	double cpu_time_used;

	#define MAX_THREADS 4

	start = clock();
	long long suma=0;
	long long sumc=0;

   omp_set_num_threads(MAX_THREADS);

   long long j;
   int tid;
   int numthreads;
	#pragma omp parallel private(j,tid)
	{
		tid = omp_get_thread_num();//线程的id标记

		for(j=tid;j<7;j+=MAX_THREADS)
		{	
			//printf("j=%d thread id=%d\n",j,omp_get_thread_num());
			long long sumb=0;
			for(long long k=j+1;k<6;k++)
			{	
				//printf("j=%d k=%d,id=%d\n",j,k,omp_get_thread_num());
				for(long long i=0;i<200000000;i++)
				{
					suma+=i;
				}
				sumb+=k;
			}
			//printf("sumb=%d thread id=%d\n",sumb,omp_get_thread_num());
			//#pragma omp critical
			//#pragma omp atomic
			sumc+=sumb;
		}
	//printf("sumc=%d thread id=%d\n",sumc,omp_get_thread_num());
	}

	end = clock();
	std::cout<<"sumc="<<sumc << std::endl; 
	cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
	std::cout<<"time="<<cpu_time_used << std::endl; 


    std::cout << std::endl; 

    return 0; 
}

下面再来看一个例子,循环的创建不同数量的线程组进行并行计算:

其中创建了6个线程组进行比较,分别使用1/2/3/4/5/6个线程进行计算。

#include <iostream> 
#include <vector> 
#include <string> 
#include <time.h>
#include <omp.h>

int main(int argc, const char * argv[]) { 
	clock_t start, end;
	double cpu_time_used;

	start = clock();

	for(int j1=1;j1<=6;j1++){ //c尝试多个不同线程数的线程组来运行比较
	omp_set_num_threads(j1);

	long long suma;
	long long sumc;
	suma=0;
	sumc=0;

	long long j;
	int tid;
	int numthreads;
		#pragma omp parallel private(j,tid)
		{
			tid = omp_get_thread_num();//线程的id标记
			numthreads = omp_get_num_threads();
			//printf("number threads=%d id=%d\n",numthreads,omp_get_thread_num());

			for(j=tid;j<7;j+=numthreads)
			{	
				//printf("j=%d thread id=%d %d\n",j,omp_get_thread_num(),j1);
				long long sumb=0;
				for(long long k=j+1;k<6;k++)
				{
					for(long long i=0;i<200000000;i++)
					{
						suma+=i;
					}
					sumb+=k;
				}
				//printf("sumb=%d thread id=%d %d\n",sumb,omp_get_thread_num(),j1);
				#pragma omp atomic
				sumc+=sumb;
			}

			#pragma omp single
			{
				printf("sumc=%d thread id=%d %d\n",sumc,omp_get_thread_num(),j1);
				end = clock();
				std::cout<<"sumc="<<sumc << std::endl; 
				cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
				std::cout<<"time="<<cpu_time_used <<"\n"<< std::endl; 
			}

		}
	}

    std::cout << std::endl; 

    return 0; 
}


最后来看以下全局定义的数组的副本维护情况

注意到:
对数组j进行了副本维护,但显然在超出数据范围后,各线程仍然能够访问和使用。
所以在使用数组时要特别注意。这也是c++、fortran这类编译语言的特点,不对数组的范围进行维护,一切都要代码编写者自己注意。

比如:

#include <iostream> 
#include <vector> 
#include <string> 
#include <omp.h>

int main(int argc, const char * argv[]) { 


 int k = 20;
 int j[10];
 j[0] = 20;
#pragma omp parallel for private(k,j)
    for ( k=0; k < 11; k++)
    {
              //printf("k->%d\t", k);
              j[k] = k*k;
              printf("j[%d]->%d\t",k,j[k]);
              
    }
    printf("\n");

    for  ( k=0; k < 11; k++)
    {
        printf("j[%d]->%d\t",k,j[k]);
    }
    printf("\n");

#pragma omp parallel for
    for ( k=0; k < 11; k++)
    {
              //printf("k->%d\t", k);
              j[k] = k*k;
              printf("j[%d]->%d\t",k,j[k]);
              
    }
    printf("\n");

    for  ( k=0; k < 11; k++)
    {
        printf("j[%d]->%d\t",k,j[k]);
    }
    printf("\n");



    return 0; 
}

小结

针对直接的需求问题,在学习基础上给出了openmp的理解,并进行了代码实践,应该说解决了openmp的初步使用问题。
因为没有需求的原因,也没有继续深入下去应用更为复杂的功能。简单其实也是我追求的,尽可能使用一些简单的东西,对于后期的理解来说是更有好处的,因为很有可能用完这段之后很久都不会再用了,发出来下来当做记录吧。

参考:

  1. https://www.openmp.org/resources/tutorials-articles/
  2. Hands-On Introduction to OpenMP, Mattson and Meadows, from SC08 (Austin)
  3. https://blog.csdn.net/qq_40379678/article/details/107788716?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-5&spm=1001.2101.3001.4242
  4. 其它一些csdn不少文章,也浏览过,没有一一记录下来,有兴趣自行搜索。

标签:常见问题,--,并行,long,omp,线程,openmp,include
来源: https://blog.csdn.net/xenonhu/article/details/112044800

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有