作者:马健
邮箱: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开发过程中还发现以下限制:
- 不能用于迭代过程,例如用中位切分(Median cut)算法做色彩量化。迭代过程都是逐步逼近,某个线程从中间横插一杠子进来算怎么回事?
- 不宜用于滚动算法,例如积分图计算、局部直方图计算等。这种情况不是不能并行,而是各线程初始化时需要消耗大量时间和资源,可能得不偿失。
- 对于存在资源竞争的场合需要仔细测试,以定量数据为基准评估究竟是否值得上OpenMP。理论上说可以用线程同步机制解决关键资源的并发访问冲突,用多开缓冲区的方式解决内存冲突,但是这些解决方案都要耗时,最终可能导致用OpenMP的时间比不用还长。例如在一个循环体内需要频繁对同一个共享vector进行push和pop操作,如果每次都用critical加锁,其实并行处理的意义也就不大了。
- 最好把用了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的具体实现:
- 如果实现得比较low,不能检测到这种嵌套调用并进行相应处理,就会出现线程风暴。以8线程CPU为例,在函数A里会并行启动8个线程执行循环体,然后每个线程在执行到对函数B的调用时,又会在函数B中再启动8个线程,最终一共有8×8=64个线程在同时运行。如果再进行一层嵌套,则会有64×8=512个线程。这么多的线程数量远远超过CPU物理线程数,排队等待线程调度的时间都可能超过线程的执行时间。
- 如果实现得比较好,就会自动检测到这种嵌套调用,并且在发现嵌套后就自动把多线程变成单线程。还是以上面的代码为例,函数A中启动8个线程并行执行,调用到函数B时发现已经是处于多线程并行状态,就自动把函数B中的循环体按照单线程执行,所以最终同时运行的线程数就是8个。
以Intel C++ Compiler(ICC)为例,早期版本提供的OpenMP静态库就不能检测并防止线程风暴,后来版本提供的OpenMP动态库则能有效检测并防止线程风暴,所以后来版本干脆就不再提供OpenMP静态库,一律要求用动态库。
但是如果混用不同C++编译器所编译的OpenMP代码,则可能还是会产生线程风暴,因为一家的OpenMP不一定能识别另一家的OpenMP已经在运行。举个例子,如果用微软的VC 2008开发一款图像处理软件,但为了加速性能又调用了Intel的Integrated Performance Primitives(IPP)库,并且link了IPP的多线程版本lib,则在VC代码中用OpenMP并行调用IPP库函数时就要格外小心,因为IPP是用ICC编译的,在IPP库函数内部确实能够防止ICC自己的OpenMP嵌套调用,但它不能识别VC的OpenMP状态,所以二者混用就可能产生线程风暴,表现出的结果就是速度变得相当慢。
Intel开发社区对此给出的建议是:任何时候只使用一家编译器的OpenMP。以上面举例的情况为例,就是只在VC中使用OpenMP,然后link到IPP的单线程版lib,就不会出现风暴,VC自己总能识别自己的OpenMP状态,避免嵌套。
如果实在不能避免混用,或者说自己都搞不清编译器是否能自动避免嵌套,那就是像前面说的一样自己维护一个使用了OpenMP的函数清单,调用清单上的函数时,就不要再并行处理了。IPP每个版本都会附上这么一个清单。
(完)