|
|
[交流]
openMP并行入门 已有2人参与
前言:下面是在参加完Calculquebec的一个关于OpenMP的workshop之后,结合他们给的教程、以及网上搜索后写成的。以前从没有接触过openMP,因此,难免有错误之处。时间不充裕,因此,其中的代码既有FORTRAN的,又有C++的。不过,为了方便读者秒懂,我把大量的细节删去了,比如变量的声明,初值等。请自行脑补。
1. openMPI和openMP的区别
现在假设有两条生产线,同时执行一个任务,那么存在两种情形。一种情况是这两条生产线各自有自己的原材料/成品仓库,还有一种情况是他们共享一个原料成品仓库。这的原料成品仓库当然是指内存,而生产线就是处理器CPU。这两种情况,前者属于分布式并行,MPI方式,即常见的大型服务器,有很多节点并行运行同一计算任务的情况。后者属于openMP方式并行,即单机多核多线程电脑的并行,只有一个内存,所以要共享。从这里看,二者的区别不大,但实际实施上差别非常巨大。MPI方式需要把并行的代码和实际的工作代码结合起来,编程上灵活但复杂。而openMP则简单多了,大多数情况下只需在原有的串行代码上增加一些并行语言指令即可,工作代码仅需很小幅度的修改。此外,目前流行的C++/Fortran编译器都已经内置了对openMP的指令和库的支持。而MPI的指令和库文件则需要额外的编译安装。当然,MPI在集群服务器上的表现也更为强大,不像openMP只能在多核的工作站单机上逞威风。
2. 并行需要解决的问题,以openMP为例
![openMP并行入门]()
先来解释几个简单名词。首先是指令directive,openMP的实现是通过在串行code中加openMP的并行指令实现的。这些指令在FORTRAN中是 !$OMP开头的。而在FORTRAN中,!开头的都是注释语句。一般情况下,注释语句是不被编译的。但是,如果你编译的时候指定了-fopenmp,则编译器会对所有以!$开头的openMP语句加以编译。真是一个聪明的解决办法。这使得你可以既能以串行方式编译,也能以并行方式编译,而无需对code进行任何修改。
其次,一个最重要的概念是变量的私有(PRIVATE)和公有(SHARED)属性。这个好理解,每个任务都有自己所在的处理器序号、循环计数器等等,这些当然不能共享。但是,由于openMP的共享内存的特征,如果你不把这些私有的变量声明出来,就会出大乱子。下面的例子中会解释,一看就能明白。
(1) 任务分配机制
首先来看最简单的任务分配机制。如果你对一个FORTRAN code中打印hello的语句前后增加openMP的PARALLEL指令,如下: 那么,当你便以后执行的时候,每一个线程(Processor)都会运行这个write语句,即有几个核就会输出几个“Hello, world!”
你会说,这叫什么任务分配,不过重复执行。我们需要的是对比如1000个计算分给2个处理器,每个执行其中的500个,以节约时间。是的,我们不妨把上面最简单的情况称为齐应机制,即齐声答应,它展现了并行指令对工作语句最简单的影响。然后,我们来谈谈真实的任务分配。
第一种情况,以上面的例子为例,为了进一步形象化,我们可以把任务想象成要生产1000双鞋。串行的话,可以对一个生产鞋的指令加上DO循环指令,循环1000次。并行的话,我们只需在循环指令语句前后套上一个openMP的PARALLEL FOR语句,编译器就会很聪明地把任务平均分配给每个处理器。 其中的DO ... END DO是串行的循环语句。而套上的!$开头的指令则将code转化为并行。其中的PRIVATE, SHARED则是对私有、共有变量的区分,细节请脑补,你懂得。
上面这种分配任务的方式,把循环平均分配到可用处理器上去,我们称为平均分配机制。
但是,有的时候,你想指定一号生产线生产男鞋,二号生产线生产女鞋。这种情形下你觉得应该怎么实现?你会说,很简单,用条件判断语句IF进行判断。是的,大概是这个样子的: 上面的code,先用一个omp_get_thread_num()获取当前处理器的序号rank,然后用IF把任务指派下去。当然,变量rank要在这段code之前先声明并赋初值,保证串行的时候也能符合其中的一个条件,不过那种情况下你只能做一种鞋了。此外,除了omp_get_thread_num(),另外一个重要的指令是omp_get_num_threads()用来获取当前总共有几个处理器。注意后者的threads是复数。
上面这种分配任务机制像不像列宁老人家的社会主义的计划经济下的强行指派方式?我们不妨称为“计划经济机制”。这种机制想对于第一种更为灵活,你可以对不同处理指派不同的任务。
不过,历史早已证明,计划经济是失败的,马克思主义的计划经济就是臭狗屎。在编程上,你遇到的问题可能是,假如你有2个处理器,但任务划分成了三块,或者更多块。作为聪明的人,你并不想强行指派,而是希望采用市场经济的方式,让处理器自行来认领任务,然后,谁先完成自己的一块,就自动来认领下一块。这样,尽可能减少时间的浪费。God,这真是天才的想法!这种分配机制,我们称为“能者多劳机制”,openMP中使用SECTIONS指令。 注意这最后一种模式非常灵活。不过,这三种并行解决机制,都只能适于各子任务之间不相互依赖的情形。如果后一任务的开始依赖于前一任务的结果,那么这种情形下能并行吗?请看下面的关于这一问题的讨论。
(2) 冲突解决机制
因为前面考虑的任务,都是数据之间不互相依赖的情形。但是,我们在并行化一段已有的串行code时,遇到的更多的是各子任务的数据结果相互依赖的情形。考虑一个很常见的情形:假如有1000个箱子,每个箱子里有未知数目的鞋子,你需要知道总的鞋子数。在串行的情况下,你编写了一个循环code,依次统计每一个箱子里面的鞋子数,把每个箱子中的鞋子数加到当前总数中并写到一块小黑板上。(下面改用C++ pseudo code) 对这样的一个code进行openMP的套用并行语句, 结果当然会出错。因为套用并行指令的结果,是把箱子平均分配给每个处理器,每个处理器统计完自己所计总数之后,都会去改写小黑板上的鞋子总数——一个不可能私有化的公有变量。你说,这好办,串行code修改成每个箱子的鞋子数作为一个数组元存到一个数组中去,最后数组元求和;这样并行时每个处理器都只会修改不同的数组元。问题是,这种方式不仅耗内存,而且增加了额外的求和开销。串行code不太可能这样编写。因此,我们不讨论专为后来增加openMP而特别设计code的特殊情形,而只考虑最可能出麻烦的一般情形。
这种情况下,openMP提供的第一种解决思路是“互斥锁机制”。例如,在求和语句前,增加一个critical语句或atomic语句, 增加这样一个语句后,不允许子任务同时运行。换句话说,处理器挨个统计,最后加和。且对最后结果加和时是累加而非改写。这即使所谓互斥锁。这样只是避免了出错,并行后跟没并行一样,还增加了并行的管理开销。结果比串行还慢。critical和atomic有什么区别呢?没看太清楚,一般的教程只是提到二者效果等同,但atomic方式造成的开销小于critical。
从上面看,你会说,尼玛互斥锁简直就是坑爹,那样的并行有神马意义?直接每个处理器统计个总数,最后加起来不就完了。没错,这种处理方式就是openMP提供的第二种冲突解决机制,称为“局部求和机制”。它的缺点是,必须对原来的串行code进行修改。举例: 上面的代码在其中增加了私有变量local_sum,用来存储每个处理器统计总数的变量local_sum。因此,omp for后面的代码是分别在个处理器上进行的,其统计结果是私有的。最后使用一个omp atomic语句,其作用是新辟一个线程对所有的local_sum求和得到总数total。
并行的同步与退出。上面的omp for在哪里结束并行呢?很显然,omp for循环并行在for语句结束后结束并行,转入下面的串行或者是新的并行进程。不过,默认的并行有个同步机制。此机制要求率先完成的处理器要等待其它处理器结束后才能转入下一步。上面omp for指令后面的nowait则指示率先完成计数的处理器不必等待其它处理器完成统计,直接将自己的结果传给下面的omp atomic进程,以节约时间。nowait经常出现在这类循环的并行中。
Ref:
1. openMP workshop, Calculquebec, McGill, 2014
2. openMP by Examples, http://sc.tamu.edu/shortcourses/SC-openmp/OpenMPSlides_tamu_sc.pdf
3. OpenMP发展与优势, http://blog.chinaunix.net/uid-13327770-id-2902331.html?page=2&bsh_bid=356302935
[ Last edited by ChemiAndy on 2014-2-27 at 11:05 ] |
» 收录本帖的淘帖专辑推荐
» 猜你喜欢
» 本主题相关价值贴推荐,对您同样有帮助:
|