OpenMP基础----以图像处理中的问题为例

    OpenMP2.5规范中,对于可以多线程执行的循环有如下5点约束:

1.循环语句中的循环变量必须是有符号整形,如果是无符号整形就无法使用,OpenMP3.0中取消了这个约束

2.循环语句中的比较操作必须是这样的样式:loop_variable <,<=,>,>=loop_invariant_interger

3.循环语句中必须是整数加,整数减,加减的数值必须是循环不变量

4.如果比较操作是《,《=,那么循环变量的值在每次迭代时候必须增加,反之亦然

5.循环必须是单入口,单出口,内部没有跳转语句

将循环多线程化所面临的挑战

1.循环迭代相关

因为OpenMP编译指导是对编译器发出的命令,所以编译器会将该循环编译成多线程代码,但由于循环迭代相关的存在,多线程代码将不能成功执行。

2.数据竞争

3.数据相关(以下假设为语句S2与语句S1存在数据相关):

相关的种类(相关不等于循环迭代相关):

1)流相关:S1先写某一存储单元,而后S2又读该单元

2)输出相关:两个语句写同一存储单元

3)反相关:一个语句先读一单元,然后另一语句写该单元

相关产生的方式:

1)S1在循环的一次迭代中访问存储单元L,S2在随后的一次迭代中访问L(是循环迭代相关)

2)S1和S2在同一循环迭代中访问同一存储单元L,但S1的执行在S2之前。(非循环迭代相关)

数据竞争:
      数据竞争可能是由于输出相关引起的,编译器不会进行数据竞争的检测,Intel线程检测器可以检测数据竞争。
用类似于互斥量的机制进行私有化和同步,可以消除数据竞争。
#pragma omp parallel for private(x)
       for(i=0;i<80;i++)
       {
         x=sin(i);
         if(x>0.6)x=0.6;
         printf("sin(%d)=%f\n",i,x); 
       }
6.
管理共享数据和私有数据:
private:每个线程都拥有该变量的一个单独的副本,可以私有的访问
         1)private:说明列表中的每个变量对于每个线程都应该有一个私有副本。这个私有副本用变量的默认值进行初始化
         2)firstprivate:见13数据的Copy-in 和Copy-out
         3)lastprivate:见13数据的Copy-in 和Copy-out
         4)reduction:
         5)threadprivate:指定由每个线程私有的全局变量
有三种方法声明存储单元为私有:
         1)使用private,firstprivate,lastprivate,reduction子句
         2)使用threadprivate
         3)在循环内声明变量,并且不使用static关键字
shared:所有线程都能够访问该单元,并行区域内使用共享变量时,如果存在写操作,必须对共享变量加以保护
default:并行区中所有变量都是共享的,除下列三种情况下:
          1)在parallel for循环中,循环索引时私有的。
          2)并行区中的局部变量是私有的
          3)所有在private,firstprivate,lastprivate,reduction子句中列出的变量是私有的
7.
循环调度与分块
     为了提供一种简单的方法以便能够在多个处理器之间调节工作负载,OpenMP给出了四种调度方案:
static,dynamic,runtime,guided.
     默认情况下,OpenMP采用静态平均调度策略,但是可以通过调用schedule(kind[,chunksize])子句提供循环调度信息
如:#pragma omp for schedule (kind[,chunk-size])   //chunk-size为块大小
guided根据环境变量里的设置来进行对前三种的调度
在windows环境中,可以在”系统属性|高级|环境变量”对话框中进行设置环境变量。
8.
有效地使用归约:
sum=0;
for(k=0;k<100;k++)
{
    sum=sum+func(k);
}
     为了完成这种形式的循环计算,其中的操作必须满足算术结合律和交换律,同时sum是共享的,这样循环内部都可以加给这个变量,同时又必须是私有的,以避免在相加时的数据竞争。
reduction子句可以用来有效地合并一个循环中某些关于一个或多个变量的满足结合律的算术归约操作。reduction子句主要用来对一个或多个参数条目指定一个操作符,每个线程将创建参数条目的一个私有拷贝,在区域的结束处,将用私有拷贝的值通过指定的运行符运算,原始的参数条目被运算结果的值更新。
sum=0;
#pragma omp parallel for reduction(+:sum)
for(k=0;k<100;k++)
{
    sum=sum+func(k);
}
9.
降低线程开销:当编译器生成的线程被执行时,循环的迭代将被分配给该线程,在并行区的最后,所有的线程都被挂起,等待共同进入下一个并行区、循环或结构化块。
              如果并行区域、循环或结构化块是相邻的,那么挂起和恢复线程的开销就是没必要的。
举例如下:
                #pragma omp parallel //并行区内
                {
                   #pragma omp for // 任务分配for循环
                          for(k=0;k<m;k++){
                               fun1(k);
                           }
                   #pragma omp for
                          for(k=0;k<m;k++){
                               fun2(k);
                           }
                }
10.任务分配区:
     现实中应用程序的所有性能敏感的部分不是都在一个并行区域内执行,所以OpenMP用任务分配区这种结构来处理非循环代码。
任务分配区可以指导OpenMP编译器和运行时库将应用程序中标示出的结构化块分配到用于执行并行区域的一组线程上。
举例如下:
              #pragma omp parallel //并行区内
                {
                   #pragma omp for // 任务分配for循环
                          for(k=0;k<m;k++){
                               fun1(k);
                           }
                   #pragma omp sections private(y,z)
                     {
                           #pragme omp section//任务分配section
                               {y=sectionA(x);}
                           #pragme omp section
                               {z=sectionB(x);}
                     }                   
                }
11.
使用Barrier和Nowait:
      栅障(Barrier)是OpenMP用于线程同步的一种方法。线程遇到栅障是必须等待,直到并行区中的所有线程都到达同一点。
注意:在任务分配for循环和任务分配section结构中,我们已经隐含了栅障,在parallel,for,sections,single结构的最后,也会有一个隐式的栅障。
隐式的栅障会使线程等到所有的线程继续完成当前的循环、结构化块或并行区,再继续执行后面的工作。可以使用nowait去掉这个隐式的栅障
去掉隐式栅障,例如:
                #pragma omp parallel //并行区内
                {
                   #pragma omp for nowait // 任务分配for循环
                          for(k=0;k<m;k++){
                               fun1(k);
                           }
                   #pragma omp sections private(y,z)
                     {
                           #pragme omp section//任务分配section
                               {y=sectionA(x);}
                           #pragme omp section
                               {z=sectionB(x);}
                     }                   
                }
     因为第一个 任务分配for循环和第二个任务分配section代码块之间不存在数据相关。
加上显示栅障,例如:
                              #pragma omp parallel shared(x,y,z) num_threads(2)//使用的线程数为2
                               {
                                   int tid=omp_get_thread_num();
                                   if(tid==0)
                                       y=fun1();//第一个线程得到y
                                   else 
                                        z=fun2();//第二个线程得到z
                                   #pragma omp barrier //显示加上栅障,保证y和z在使用前已有值
                                   #pragma omp for
                                           for(k=0;k<100;k++)
                                                   x[k]=y+z;
                               }
12.
单线程和多线程交错执行:
      当开发人员为了减少开销而把并行区设置的很大时,有些代码很可能只执行一次,并且由一个线程执行,这样单线程和多线程需要交错执行
举例如下:
               #pragma omp parallel //并行区
              {
                    int tid=omp_get_thread_num();//每个线程都调用这个函数,得到线程号
                     //这个循环被划分到多个线程上进行
                      #pragma omp for nowait
                      for(k=0;k<100;k++)
                            x[k]=fun1(tid);//这个循环的结束处不存在使所有线程进行同步的隐式栅障
                    #pragma omp master
                      y=fn_input_only(); //只有主线程会调用这个函数
                    #pragma omp barrier   //添加一个显示的栅障对所有的线程同步,从而确保x[0-99]和y处于就绪状态
                     //这个循环也被划分到多个线程上进行
                    #pragma omp for nowait
                      for(k=0;k<100;k++)
                         x[k]=y+fn2(x[k]); //这个线程没有栅障,所以不会相互等待
                     //一旦某个线程执行完上面的代码,不需要等待就可以马上执行下面的代码
                     #pragma omp single //注意:single后面意味着有隐式barrier
                     fn_single_print(y);
                      //所有的线程在执行下面的函数前会进行同步
                     #pragma omp master
                     fn_print_array(x);//只有主线程会调用这个函数
              } 
13.
数据的Copy-in 和Copy-out:
      在并行化一个程序的时候,一般都必须考虑如何将私有变量的初值复制进来(Copy-in ),以初始化线程组中各个线程的私有副本。
在并行区的最后,还要将最后一次迭代/结构化块中计算出的私有变量复制出来(Copy-out),复制到主线程中的原始变量中。
firstprivate:使用变量在主线程的值对其在每个线程的对应私有变量进行初始化。一般来说,临时私有变量的初值是未定义的。
lastprivate:可以将最后一次迭代/结构化块中计算出来的私有变量复制出来,复制到主线程对应的变量中,一个变量可以同时用firstprivate和lastprivate来声明。
copyin:将主线程的threadprivate变量的值复制到执行并行区的每个线程的threadprivate变量中。
copyprivate:使用一个私有变量将某一个值从一个成员线程广播到执行并行区的其他线程。该子句可以关联single结构(用于single指令中的指定变量为多个线程的共享变量),在所有的线程都离开该结构中的同步点之前,广播操作就已经完成。
14.
保护共享变量的更新操作:
     OpenMP支持critical和atomic编译指导,可以用于保护共享变量的更新,避免数据竞争。包含在某个临界段且由atomic编译指导所标记的代码块可能只由一个线程执行。
例如:#pragma omp critical
   {
              if(max<new_value) max=new_value;
         }
15.
OpenMP库函数(#include <omp.h>):
int omp_get_num_threads(void); //获取当前使用的线程个数
int omp_set_num_threads(int NumThreads);//设置要使用的线程个数
int omp_get_thread_num(void);//返回当前线程号
int omp_get_num_procs(void);//返回可用的处理核个数

下面我们来看一个具体的应用例,从硬盘读入两幅图像,对这两幅图像分别提取特征点,特征点匹配,最后将图像与匹配特征点画出来。理解该例子需要一些图像处理的基本知识,我不在此详细介绍。另外,编译该例需要opencv,我用的版本是2.3.1,关于opencv的安装与配置也不在此介绍。我们首先来看传统串行编程的方式。

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/features2d/features2d.hpp"
#include <iostream>
#include <omp.h>
int main( ){
    cv::SurfFeatureDetector detector( 400 );
    cv::SurfDescriptorExtractor extractor;
    cv::BruteForceMatcher<cv::L2<float> > matcher;
    std::vector< cv::DMatch > matches;
    cv::Mat im0,im1;
    std::vector<cv::KeyPoint> keypoints0,keypoints1;
    cv::Mat descriptors0, descriptors1;
    double t1 = omp_get_wtime( );
    //先处理第一幅图像
    im0 = cv::imread("rgb0.jpg", CV_LOAD_IMAGE_GRAYSCALE );
    detector.detect( im0, keypoints0);
    extractor.compute( im0,keypoints0,descriptors0);
    std::cout<<"find "<<keypoints0.size()<<"keypoints in im0"<<std::endl;
    //再处理第二幅图像
    im1 = cv::imread("rgb1.jpg", CV_LOAD_IMAGE_GRAYSCALE );
    detector.detect( im1, keypoints1);
    extractor.compute( im1,keypoints1,descriptors1);
    std::cout<<"find "<<keypoints1.size()<<"keypoints in im1"<<std::endl;
    double t2 = omp_get_wtime( );
    std::cout<<"time: "<<t2-t1<<std::endl;
    matcher.match( descriptors0, descriptors1, matches );
    cv::Mat img_matches;
    cv::drawMatches( im0, keypoints0, im1, keypoints1, matches, img_matches );
    cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
    cv::imshow( "Matches", img_matches );
    cv::waitKey(0);
    return 1;
}

很明显,读入图像,提取特征点与特征描述子这部分可以改为并行执行,修改如下:

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/features2d/features2d.hpp"
#include <iostream>
#include <vector>
#include <omp.h>
int main( ){
    int imNum = 2;
    std::vector<cv::Mat> imVec(imNum);
    std::vector<std::vector<cv::KeyPoint>>keypointVec(imNum);
    std::vector<cv::Mat> descriptorsVec(imNum);
    cv::SurfFeatureDetector detector( 400 );    cv::SurfDescriptorExtractor extractor;
    cv::BruteForceMatcher<cv::L2<float> > matcher;
    std::vector< cv::DMatch > matches;
    char filename[100];
    double t1 = omp_get_wtime( );
#pragma omp parallel for
    for (int i=0;i<imNum;i++){
        sprintf(filename,"rgb%d.jpg",i);
        imVec[i] = cv::imread( filename, CV_LOAD_IMAGE_GRAYSCALE );
        detector.detect( imVec[i], keypointVec[i] );
        extractor.compute( imVec[i],keypointVec[i],descriptorsVec[i]);
        std::cout<<"find "<<keypointVec[i].size()<<"keypoints in im"<<i<<std::endl;
    }
    double t2 = omp_get_wtime( );
    std::cout<<"time: "<<t2-t1<<std::endl;
    matcher.match( descriptorsVec[0], descriptorsVec[1], matches );
    cv::Mat img_matches;
    cv::drawMatches( imVec[0], keypointVec[0], imVec[1], keypointVec[1], matches, img_matches );
    cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
    cv::imshow( "Matches", img_matches );
    cv::waitKey(0);
    return 1;
}

两种执行方式做比较,时间为:2.343秒v.s. 1.2441秒

在上面代码中,为了改成适合#pragma omp parallel for执行的方式,我们用了STL的vector来分别存放两幅图像、特征点与特征描述子,但在某些情况下,变量可能不适合放在vector里,此时应该怎么办呢?这就要用到openMP的另一个工具,section,代码如下:

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/features2d/features2d.hpp"
#include <iostream>
#include <omp.h>
int main( ){
    cv::SurfFeatureDetector detector( 400 );    cv::SurfDescriptorExtractor extractor;
    cv::BruteForceMatcher<cv::L2<float> > matcher;
    std::vector< cv::DMatch > matches;
    cv::Mat im0,im1;
    std::vector<cv::KeyPoint> keypoints0,keypoints1;
    cv::Mat descriptors0, descriptors1;
    double t1 = omp_get_wtime( );
#pragma omp parallel sections
    {
#pragma omp section
        {
            std::cout<<"processing im0"<<std::endl;
            im0 = cv::imread("rgb0.jpg", CV_LOAD_IMAGE_GRAYSCALE );
            detector.detect( im0, keypoints0);
            extractor.compute( im0,keypoints0,descriptors0);
            std::cout<<"find "<<keypoints0.size()<<"keypoints in im0"<<std::endl;
        }
#pragma omp section
        {
            std::cout<<"processing im1"<<std::endl;
            im1 = cv::imread("rgb1.jpg", CV_LOAD_IMAGE_GRAYSCALE );
            detector.detect( im1, keypoints1);
            extractor.compute( im1,keypoints1,descriptors1);
            std::cout<<"find "<<keypoints1.size()<<"keypoints in im1"<<std::endl;
        }
    }
    double t2 = omp_get_wtime( );
    std::cout<<"time: "<<t2-t1<<std::endl;
    matcher.match( descriptors0, descriptors1, matches );
    cv::Mat img_matches;
    cv::drawMatches( im0, keypoints0, im1, keypoints1, matches, img_matches );
    cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
    cv::imshow( "Matches", img_matches );
    cv::waitKey(0);
    return 1;
}

上面代码中,我们首先用#pragma omp parallel sections将要并行执行的内容括起来,在它里面,用了两个#pragma omp section,每个里面执行了图像读取、特征点与特征描述子提取。将其简化为伪代码形式即为:

 1 #pragma omp parallel sections 2 { 3     #pragma omp section 4     { 5         function1(); 6     } 7   #pragma omp section 8     { 9         function2();10     }11 }

意思是:parallel sections里面的内容要并行执行,具体分工上,每个线程执行其中的一个section,如果section数大于线程数,那么就等某线程执行完它的section后,再继续执行剩下的section。在时间上,这种方式与人为用vector构造for循环的方式差不多,但无疑该种方式更方便,而且在单核机器上或没有开启openMP的编译器上,该种方式不需任何改动即可正确编译,并按照单核串行方式执行。

以上分享了这两天关于openMP的一点学习体会,其中难免有错误,欢迎指正。另外的一点疑问是,看到各种openMP教程里经常用到private,shared等来修饰变量,这些修饰符的意义和作用我大致明白,但在我上面所有例子中,不加这些修饰符似乎并不影响运行结果,不知道这里面有哪些讲究。

在写上文的过程中,参考了包括以下两个网址在内的多个地方的资源,不再一 一列出,在此一并表示感谢。

http://blog.csdn.net/drzhouweiming/article/details/4093624
http://software.intel.com/zh-cn/articles/more-work-sharing-with-openmp

OpenMP嵌套并行:

http://blog.csdn.net/zhuxianjianqi/article/details/8287937

一些优秀博客的加速例子:

http://www.cnblogs.com/LBSer/p/4604754.html

http://www.cnblogs.com/louyihang-loves-baiyan/p/4913164.html

参考文献:

http://www.cnblogs.com/yangyangcv/archive/2012/03/23/2413335.html

时间: 2024-10-30 19:34:08

OpenMP基础----以图像处理中的问题为例的相关文章

线程同步-图像处理中跟踪模块和动作识别模块的同步问题

问题描述 图像处理中跟踪模块和动作识别模块的同步问题 我们一个图像处理算法包括跟踪模块和动作处理模块,跟踪模块对一帧处理完后就开启一个新线程调用动作识别模块,但动作识别模块需要的时间比较长,所以,跟踪模块已经处理下一帧了,但动作识别模块还没处理完当前帧,跟踪模块又会开一个新的线程,这样就无法同步,请问该怎么解决,谢谢! 解决方案 http://blog.csdn.net/shanghaiqianlun/article/details/10958001

matlab图像处理-图像处理中去阴影问题

问题描述 图像处理中去阴影问题 需要通过RGB三通道模型去除图像阴影的程序,用MATLAB 实现,求大神指导 解决方案 阴影一般符合一定的颜色特征(所有阴影的颜色接近且三通道符合特定颜色的关系,类似红色筛选),通过遍历像素点进行三通道的阈值筛选

web前端-WEB前端基础 下列代码中,错误的是()?

问题描述 WEB前端基础 下列代码中,错误的是()? A. 01发票信息 发票抬头: 发票内容: B.发票信息 发票抬头: 发票内容: C.% D. 搜索"恒高科技": 解决方案 web前端基础Web前端基础[WEB基础]⑤一些前端小东西 解决方案二: 你这个是不是有格式的啊? 解决方案三: 我表示看不懂,是不是内容不全啊 相关文章 代码-WEB前端基础 答案为什么是B html-如何才能系统的学习web前端呢 web前端-css遇到十分基础的问题,求解释 web前端框架挑选问题,踊跃

js基础之DOM中document对象的常用属性方法详解_javascript技巧

-----引入 每个载入浏览器的 HTML 文档都会成为 Document 对象. Document 对象使我们可以从脚本中对 HTML 页面中的所有元素进行访问. 属性 1  document.anchors  返回对文档中所有 Anchor 对象的引用.还有document.links/document.forms/document.images等 2  document.URL       返回当前文档的url 3  document.title       返回当前文档的标题 4  do

js基础之DOM中元素对象的属性方法详解_javascript技巧

在 HTML DOM (文档对象模型)中,每个部分都是节点. 节点是DOM结构中最基本的组成单元,每一个HTML标签都是DOM结构的节点. 文档是一个    文档节点 . 所有的HTML元素都是    元素节点 所有 HTML 属性都是    属性节点 文本插入到 HTML 元素是    文本节点 注释是    注释节点. 最基本的节点类型是Node类型,其他所有类型都继承自Node,DOM操作往往是js中开销最大的部分,因而NodeList导致的问题最多.要注意:NodeList是'动态的',

C#中正则表达式应用一例

//蛙蛙推荐:C#中正则表达式应用一例//我上次的那个问题用C#来解决了,贴一下先//首先把本文所有代码复制保存为arr_reg.cs//然后把下面的代码用CSC编译成控制台程序后就可以看到结果了,编译命令如下://csc /t:exe arr_reg.csusing System;using System.Text.RegularExpressions;public class RegexTest{ public static void wawa_reg(){ string[] arr=new

PS基础知识:图像处理中的三色原理,三原色

  (一) 三基色原理 在中学的物理课中我们可能做过棱镜的试验,白光通过棱镜后被分解成多种颜色逐渐过渡的色谱,颜色依次为红.橙.黄.绿.青.蓝.紫,这就是可见光谱.其中人眼对红.绿.蓝最为敏感,人的眼睛就像一个三色接收器的体系,大多数的颜色可以通过红.绿.蓝三色按照不同的比例合成产生.同样绝大多数单色光也可以分解成红绿蓝三种色光.这是色度学的最基本原理,即三基色原理.三种基色是相互独立的,任何一种基色都不能有其它两种颜色合成.红绿蓝是三基色,这三种颜色合成的颜色范围最为广泛.红绿蓝三基色按照不同

Photoshop基础教程:平面设计中的立体透视

导言:有些朋友喜欢用ps 做一些立体物件,或者画一些3维场景,却苦于形体不准确或者没有强烈的视觉冲击.这与没有良好的立体感有一定关系,尤其是透视的基础,对于我们这些不用3ds  max软件的 photoshop 爱好者来说学学透视满重要的. 记得大学学工程制图的时候学了一点~ 虽然忘了很多,但对现在的设计应用还是蛮有帮助的.下次再画透视很强的东西,就可以先用画笔打个稿子了~ 这样就不会太走形. 一.透视图的意义设计需要用图来表达构思.在广告艺术.建筑学.室内设计.雕塑设计.装饰设计和工业设计以及

基础-java 程序中如何获取所有类 或者带注解的类

问题描述 java 程序中如何获取所有类 或者带注解的类 最近写了一个使用注解的mvc的spring框架,这个框架的基础就是要读取到工程中的带有注解的类. 我的实现方法是将工程中的目录全部读取匹配得到.class后缀的所有路径的list,再用class.forname加载对应的类并且将注解读取判断是否为定义的注解.然后使用一个map装起来,在用到的时候拿出来.但是问题来了,我将项目打包jar之后程序是不能够读取到文件的.也就是说获取不到类的全路径如:cn.com.smq.Test 这样的字符串.