OpenMP使用笔记

作者:马健
邮箱:stronghorse_mj@hotmail.com
主页:https://www.cnblogs.com/stronghorse/

CEP从v6.00开始使用OpenMP并行处理架构来获取更快的图像处理速度,本文是对开发过程中碰到的一些问题的记录,仅供软件开发人员参考,普通用户请勿乱入。

一、OpenMP是什么?

百度百科中对OpenMP的解释是:

OpenMp提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的pragma来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。

简单粗暴的无责任解释就是:普通C++的执行顺序是串行执行,执行完一条指令才能执行下一条指令。改成并行架构后,可以同时并行执行多条指令,在指令总数不变的情况下所需的总执行时间就会减小。OpenMP说的“并行”,其实是线程之间的并行,即原先是单线程的软件,改成OpenMP版就成了多线程并行执行的软件,类似网络下载工具中的多线程下载工具。而且OpenMP在缺省情况下会自动用满CPU的物理线程数,即如果是在8线程的CPU上跑,OpenMP在缺省情况下就会自动启动8线程的线程池进行并行处理。所以运行带OpenMP的软件时CPU负载比较重,有时候甚至都能影响到鼠标指针的反应速度。

OpenMP的官方网站见这里,其中包括最新的规范文档等:
https://www.openmp.org/

如果只是想简单了解OpenMP编程,建议看这里:
https://www.cnblogs.com/yangyangcv/archive/2012/03/23/2413335.html

如果想详细了解OpenMP编程,可以看:
雷洪编著.多核异构并行计算OpenMP4.5 C/C++篇[M].北京:冶金工业出版社,2018.04. ISBN 978-7-5024-7657-1
如果想进一步了解OpenMP内部的实现机理,可以看:
罗秋明,明仲,刘刚等编著.OpenMP编译原理及实现技术[M].北京:清华大学出版社,2012.05. ISBN 978-7-302-27298-4

二、OpenMP的作用有多大?

按照一般人的理解,单线程变成多线程并行,所需执行时间应该是线程数的倒数,例如在8线程CPU上执行,所需时间就应该是原来的八分之一。但是在CEP开发过程中,我从来就没有遇见过这么美妙的事情,在我用的8线程CPU Intel i7 870上,CEP单个图像处理功能在改得最好的情况下,实测的OpenMP处理时间最多只能缩短到原先单线程处理时间的三分之一,如果代码改得有毛病,大量使用critical等线程同步器,甚至有可能比单线程执行时间更长。

虽然现实没有想象的那么美好,但能快一点是一点,想想看原先要达到人类忍受极限的7秒处理,现在可能只需要2秒多就能完成,也是可以暗爽一下的事情。而且i7 870毕竟是10多年前的CPU,以后如果换成16线程或更多线程的CPU,还能进一步提速,也算给花钱买好CPU的人一个安慰。至于那些天天把“性价比”挂在嘴边的人,就没有必要去关心了。

三、OpenMP使用有哪些限制?

除了OpenMP语法上的一些要求,比如说parallel for的循环变量必须是int类型等,在CEP开发过程中还发现以下限制:

  1. 不能用于迭代过程,例如用中位切分(Median cut)算法做色彩量化。迭代过程都是逐步逼近,某个线程从中间横插一杠子进来算怎么回事?
  2. 不宜用于滚动算法,例如积分图计算、局部直方图计算等。这种情况不是不能并行,而是各线程初始化时需要消耗大量时间和资源,可能得不偿失。
  3. 对于存在资源竞争的场合需要仔细测试,以定量数据为基准评估究竟是否值得上OpenMP。理论上说可以用线程同步机制解决关键资源的并发访问冲突,用多开缓冲区的方式解决内存冲突,但是这些解决方案都要耗时,最终可能导致用OpenMP的时间比不用还长。例如在一个循环体内需要频繁对同一个共享vector进行push和pop操作,如果每次都用critical加锁,其实并行处理的意义也就不大了。
  4. 最好把用了OpenMP的函数列个清单,调用的时候注意小心不要陷入线程风暴,具体后面再讲。

另外还有一个隐含的对人的限制:既然OpenMP是多线程的,那么写OpenMP代码的人就应该对多线程非常熟悉才行,尤其是在需要线程同步的时候,如果经验不足就可能要看着bug发呆,因为未同步导致的共享资源冲突bug,比如说缓冲区越界,IDE自动中断报错的地方可能和真正出问题的地方相聚甚远。

当然以上限制也并非绝对禁区。比如说CEP中的某个递推算法,在处理单通道灰度图像时我无论如何也不能将此递推过程并行化,但在处理3通道彩色图像时,各通道允许单独处理,于是就把三个通道分开,并行调用3次单通道处理函数对3个通道分别处理,再合并成3通道结果图,最终在处理彩色图时也能达到提速的效果。为此付出的代价就是缓冲区消耗是以前的3倍,所以用了OpenMP后我只敢发行64位版,32位版可怜的地址空间真心扛不住啊。

四、线程风暴

前面一直在说OpenMP是基于多线程的,即碰到OpenMP并行域,软件就会自动启动规定数量(缺省数量是CPU的物理线程数)的线程。那么如果出现了OpenMP嵌套调用,就有可能会引发线程风暴。举个例子:函数A里用OpenMP并行执行一个循环体,循环体内部又调用了函数B,在B中同样用OpenMP并行执行一个循环体,参见下面的示例代码,那么在函数A的执行过程中,系统究竟会启动几个线程?

void Fun_B()
{
#pragma omp parallel for
    for (int i = 0; i < 1000; i++) {
      ……
    }
}

void Fun_A()
{
#pragma omp parallel for
    for (int i = 0; i < 1000; i++) {
      Fun_B();
    }
}

void main()
{
    // 执行这一句的过程里,究竟会启动几个线程?
    Func_A();
}

这个问题的答案完全取决于编译器对OpenMP的具体实现:

  1. 如果实现得比较low,不能检测到这种嵌套调用并进行相应处理,就会出现线程风暴。以8线程CPU为例,在函数A里会并行启动8个线程执行循环体,然后每个线程在执行到对函数B的调用时,又会在函数B中再启动8个线程,最终一共有8×8=64个线程在同时运行。如果再进行一层嵌套,则会有64×8=512个线程并行。这么多的线程数量远远超过CPU物理线程数,排队等待线程调度的时间都可能超过线程的执行时间。
  2. 如果实现得比较好,就会自动检测到这种嵌套调用,并且在发现嵌套后就自动把多线程变成单线程。还是以上面的代码为例,函数A中启动8个线程并行执行,调用到函数B时发现已经是处于多线程并行状态,就自动把函数B中的循环体串行执行,而不是启动多线程并行执行,所以最终同时运行的线程数就是8个。

以Intel C++ Compiler(ICC)为例,早期ICC提供的OpenMP静态库就不能检测并防止线程风暴,但OpenMP动态库则能有效检测并防止线程风暴,所以后来从ICC v13.0开始干脆就不再提供OpenMP静态库,一律要求用动态库,为此在Intel开发社区还吵吵过一阵,一堆人各种不习惯。

但是如果混用不同C++编译器所编译的OpenMP代码,则仍然可能产生线程风暴,因为一家的OpenMP不一定能识别另一家的OpenMP已经在运行。举个例子,如果用微软的VC 2008开发一款图像处理软件,但为了加速性能又调用了Intel的Integrated Performance Primitives(IPP)库,并且link了IPP的多线程版本lib,则在VC代码中用OpenMP并行调用IPP库函数时就要格外小心,因为IPP是用ICC编译的,在IPP库函数内部确实能够防止ICC自己的OpenMP嵌套调用,但它不能识别VC的OpenMP状态,所以二者混用就可能产生线程风暴,表现出的结果就是速度变得相当慢,或者因为资源冲突而出各种幺蛾子,比如说用UIC解码出来的图像是花的。

Intel开发社区对此给出的建议是:任何时候只使用一家编译器的OpenMP。以上面举例的情况为例,就是只在VC中使用OpenMP,然后link到IPP的单线程版lib,就不会出现风暴,VC自己总能识别自己的OpenMP状态,避免嵌套。所以IPP v7.1的多线程版lib文件就被从IPP安装包中剥离,需要用单独的安装包进行安装;IPP v8虽然把多线程版lib又集成到了安装包里,但需要手工选择安装,缺省是不装的;从IPP v8.2开始干脆直接把多线程库标注为deprecated。

如果实在不能避免混用,或者说自己都搞不清编译器是否能自动避免嵌套,那就像前面说的一样自己维护一个使用了OpenMP的函数清单,在调用清单上的函数时,就不要再并行处理了。IPP每个版本都会附上这么一个清单,供用户调用时参考。

有些第三方库未使用OpenMP,而是用自己的线程池进行并行处理,则在OpenMP调用时同样要加以小心。例如libwebp,允许在调用解码接口时指定是否要启动多线程模式,如果是则在解码时启动自己的线程池进行并行解码以加快解码速度,这个线程池与OpenMP的线程池显然互不认识,极易引发线程风暴。所以在调用libwebp解码webp图像时,要么指定不使用libwebp的多线程模式,要么就只串行调用libwebp的多线程解码接口。

五、运行库支持

VC 2008开发的OpenMP应用软件在运行时需要VC 2008运行库的支持,否则在运行时就会报告“应用程序的并行配置不正确”,装上VC 2008运行库就没事了。具体请百度“应用程序的并行配置不正确”。

VC运行库其实就是一堆动态库(DLL文件),所以虽然没有直接证据,但我怀疑VC和ICC一样,都是用动态库防止OpenMP嵌套,从而抑制线程风暴。

Intel的人在Intel社区简单解释过ICC中OpenMP动态库与静态库在防止嵌套方面的区别,原贴一时找不到,我记得的大致意思是:如果link到OpenMP的静态lib,那么OpenMP调用相当于inline,会散布在各个调用处,相互之间没啥关联,所以就不能检测到嵌套调用。如果link到OpenMP的动态lib,那么所有OpenMP调用都要共享OpenMP DLL中的唯一实现(操作系统自动保证单一进程空间中同一个DLL只能有一个实例),所以在DLL中做标记就能检测到嵌套调用,再根据检测结果决定是起多线程并行执行还是直接串行执行,就不会引发线程风暴。

每家编译器都有自己实现的OpenMP DLL,每家DLL又只认自己设置的标记,所以当进程空间中同时存在多家OpenMP DLL时,即使每个OpenMP DLL都能防止自己嵌套调用,多个OpenMP DLL之间仍然不能防止互相嵌套。按照Intel社区建议只使用一家编译器的OpenMP,就是整个进程空间中只有一个OpenMP DLL,才能真正防止嵌套调用。

(完)  

posted @ 2022-02-15 11:35  strnghrs  阅读(2525)  评论(1编辑  收藏  举报