参考:http://blog.sina.com.cn/s/blog_7462bf390101d8sd.html

最近在看多核编程。简单来说,由于现在电脑CPU一般都有两个核,4核与8核的CPU也逐渐走入了寻常百姓家,传统的单线程编程方式难以发挥多核CPU的强大功能,于是多核编程应运而生。按照我的理解,多核编程可以认为是对多线程编程做了一定程度的抽象,提供一些简单的API,使得用户不必花费太多精力来了解多线程的底层知识,从而提高编程效率。这两天关注的多核编程的工具包括OpenMP和TBB。按照目前网上的讨论,TBB风头要盖过OpenMP,比如OpenCV过去是使用openMP的,但从2.3版本开始抛弃OpenMP,转向TBB。但我试下来,TBB还是比较复杂的,相比之下,OpenMP则非常容易上手。因为精力和时间有限,没办法花费太多时间去学习TBB,就在这里分享下这两天学到的OpenMP的一点知识,和大家共同讨论。

OpenMP支持的编程语言包括C语言、C++和Fortran,支持OpenMP的编译器包括Sun Studio,Intel Compiler,Microsoft Visual Studio,GCC。我使用的是Microsoft Visual Studio 2008,CPU为Intel i5 四核,首先讲一下在Microsoft Visual Studio 2008上OpenMP的配置。非常简单,总共分2步:

(1) 新建一个工程。这个不再多讲。

(2) 建立工程后,点击 菜单栏->Project->Properties,弹出菜单里,点击 Configuration Properties->C/C++->Language->OpenMP Support,在下拉菜单里选择Yes。

至此配置结束。下面我们通过一个小例子来说明openMP的易用性。这个例子是 有一个简单的test()函数,然后在main()里,用一个for循环把这个test()函数跑8遍。

[cpp] view plaincopy
  1. #include <iostream>
  2. #include <time.h>
  3. void test()
  4. {
  5. int a = 0;
  6. for (int i=0;i<100000000;i++)
  7. a++;
  8. }
  9. int main()
  10. {
  11. clock_t t1 = clock();
  12. for (int i=0;i<8;i++)
  13. test();
  14. clock_t t2 = clock();
  15. std::cout<<"time: "<<t2-t1<<std::endl;
  16. }

编译运行后,打印出来的耗时为:1.971秒。下面我们用一句话把上面代码变成多核运行。

[cpp] view plaincopy
  1. #include <iostream>
  2. #include <time.h>
  3. void test()
  4. {
  5. int a = 0;
  6. for (int i=0;i<100000000;i++)
  7. a++;
  8. }
  9. int main()
  10. {
  11. clock_t t1 = clock();
  12. #pragma omp parallel for
  13. for (int i=0;i<8;i++)
  14. test();
  15. clock_t t2 = clock();
  16. std::cout<<"time: "<<t2-t1<<std::endl;
  17. }

编译运行后,打印出来的耗时为:0.546秒,几乎为上面时间的1/4。

由此我们可以看到OpenMP的简单易用。在上面的代码里,我们一没有额外include头文件,二没有额外link库文件,只是在for循环前加了一句#pragma omp parallel for。而且这段代码在单核机器上,或者编译器没有将OpenMP设为Yes的机器上编译也不会报错,将自动忽略#pragma这行代码,然后按照传统单核串行的方式编译运行!我们唯一要多做的一步,是从C:\Program Files\Microsoft Visual Studio 9.0\VC\redist\x86\Microsoft.VC90.OPENMP和C:\Program Files\Microsoft Visual Studio 9.0\VC\redist\Debug_NonRedist\x86\Microsoft.VC90.DebugOpenMP目录下分别拷贝vcomp90d.dll和vcomp90.dll文件到工程文件当前目录下。

对上面代码按照我的理解做个简单的剖析。

当编译器发现#pragma omp parallel for后,自动将下面的for循环分成N份,(N为电脑CPU核数),然后把每份指派给一个核去执行,而且多核之间为并行执行。下面的代码验证了这种分析。

[cpp] view plaincopy
  1. #include <iostream>
  2. int main()
  3. {
  4. #pragma omp parallel for
  5. for (int i=0;i<10;i++)
  6. std::cout<<i<<std::endl;
  7. return 0;
  8. }

会发现控制台打印出了0 3 4 5 8 9 6 7 1 2。注意:因为每个核之间是并行执行,所以每次执行时打印出的顺序可能都是不一样的。

下面我们来了谈谈竞态条件(race condition)的问题,这是所有多线程编程最棘手的问题。该问题可表述为,当多个线程并行执行时,有可能多个线程同时对某变量进行了读写操作,从而导致不可预知的结果。比如下面的例子,对于包含10个整型元素的数组a,我们用for循环求它各元素之和,并将结果保存在变量sum里。

[cpp] view plaincopy
  1. #include <iostream>
  2. int main()
  3. {
  4. int sum = 0;
  5. int a[10] = {1,2,3,4,5,6,7,8,9,10};
  6. #pragma omp parallel for
  7. for (int i=0;i<10;i++)
  8. sum = sum + a[i];
  9. std::cout<<"sum: "<<sum<<std::endl;
  10. return 0;
  11. }

如果我们注释掉#pragma omp parallel for,让程序先按照传统串行的方式执行,很明显,sum = 55。但按照并行方式执行后,sum则会变成其他值,比如在某次运行过程中,sum = 49。其原因是,当某线程A执行sum = sum + a[i]的同时,另一线程B正好在更新sum,而此时A还在用旧的sum做累加,于是出现了错误。

那么用OpenMP怎么实现并行数组求和呢?下面我们先给出一个基本的解决方案。该方案的思想是,首先生成一个数组sumArray,其长度为并行执行的线程的个数(默认情况下,该个数等于CPU的核数),在for循环里,让各个线程更新自己线程对应的sumArray里的元素,最后再将sumArray里的元素累加到sum里,代码如下

[cpp] view plaincopy
  1. #include <iostream>
  2. #include <omp.h>
  3. int main(){
  4. int sum = 0;
  5. int a[10] = {1,2,3,4,5,6,7,8,9,10};
  6. int coreNum = omp_get_num_procs();//获得处理器个数
  7. int* sumArray = new int[coreNum];//对应处理器个数,先生成一个数组
  8. for (int i=0;i<coreNum;i++)//将数组各元素初始化为0
  9. sumArray[i] = 0;
  10. #pragma omp parallel for
  11. for (int i=0;i<10;i++)
  12. {
  13. int k = omp_get_thread_num();//获得每个线程的ID
  14. sumArray[k] = sumArray[k]+a[i];
  15. }
  16. for (int i = 0;i<coreNum;i++)
  17. sum = sum + sumArray[i];
  18. std::cout<<"sum: "<<sum<<std::endl;
  19. return 0;
  20. }

需要注意的是,在上面代码里,我们用omp_get_num_procs()函数来获取处理器个数,用omp_get_thread_num()函数来获得每个线程的ID,为了使用这两个函数,我们需要include <omp.h>。

上面的代码虽然达到了目的,但它产生了较多的额外操作,比如要先生成数组sumArray,最后还要用一个for循环将它的各元素累加起来,有没有更简便的方式呢?答案是有,openMP为我们提供了另一个工具,归约(reduction),见下面代码:

[cpp] view plaincopy
  1. #include <iostream>
  2. int main(){
  3. int sum = 0;
  4. int a[10] = {1,2,3,4,5,6,7,8,9,10};
  5. #pragma omp parallel for reduction(+:sum)
  6. for (int i=0;i<10;i++)
  7. sum = sum + a[i];
  8. std::cout<<"sum: "<<sum<<std::endl;
  9. return 0;
  10. }

上面代码里,我们在#pragma omp parallel for 后面加上了 reduction(+:sum),它的意思是告诉编译器:下面的for循环你要分成多个线程跑,但每个线程都要保存变量sum的拷贝,循环结束后,所有线程把自己的sum累加起来作为最后的输出。

reduction虽然很方便,但它只支持一些基本操作,比如+,-,*,&,|,&&,||等。有些情况下,我们既要避免race condition,但涉及到的操作又超出了reduction的能力范围,应该怎么办呢?这就要用到openMP的另一个工具,critical。来看下面的例子,该例中我们求数组a的最大值,将结果保存在max里。

[cpp] view plaincopy
  1. #include <iostream>
  2. int main(){
  3. int max = 0;
  4. int a[10] = {11,2,33,49,113,20,321,250,689,16};
  5. #pragma omp parallel for
  6. for (int i=0;i<10;i++)
  7. {
  8. int temp = a[i];
  9. #pragma omp critical
  10. {
  11. if (temp > max)
  12. max = temp;
  13. }
  14. }
  15. std::cout<<"max: "<<max<<std::endl;
  16. return 0;
  17. }

上例中,for循环还是被自动分成N份来并行执行,但我们用#pragma omp critical将 if (temp > max) max = temp 括了起来,它的意思是:各个线程还是并行执行for里面的语句,但当你们执行到critical里面时,要注意有没有其他线程正在里面执行,如果有的话,要等其他线程执行完再进去执行。这样就避免了race condition问题,但显而易见,它的执行速度会变低,因为可能存在线程等待的情况。
有了以上基本知识,对我来说做很多事情都足够了。下面我们来看一个具体的应用例,从硬盘读入两幅图像,对这两幅图像分别提取特征点,特征点匹配,最后将图像与匹配特征点画出来。理解该例子需要一些图像处理的基本知识,我不在此详细介绍。另外,编译该例需要opencv,我用的版本是2.3.1,关于opencv的安装与配置也不在此介绍。我们首先来看传统串行编程的方式。

[cpp] view plaincopy
  1. #include "opencv2/highgui/highgui.hpp"
  2. #include "opencv2/features2d/features2d.hpp"
  3. #include <iostream>
  4. #include <omp.h>
  5. int main( ){
  6. cv::SurfFeatureDetector detector( 400 );
  7. cv::SurfDescriptorExtractor extractor;
  8. cv::BruteForceMatcher<cv::L2<float> > matcher;
  9. std::vector< cv::DMatch > matches;
  10. cv::Mat im0,im1;
  11. std::vector<cv::KeyPoint> keypoints0,keypoints1;
  12. cv::Mat descriptors0, descriptors1;
  13. double t1 = omp_get_wtime( );
  14. //先处理第一幅图像
  15. im0 = cv::imread("rgb0.jpg", CV_LOAD_IMAGE_GRAYSCALE );
  16. detector.detect( im0, keypoints0);
  17. extractor.compute( im0,keypoints0,descriptors0);
  18. std::cout<<"find "<<keypoints0.size()<<"keypoints in im0"<<std::endl;
  19. //再处理第二幅图像
  20. im1 = cv::imread("rgb1.jpg", CV_LOAD_IMAGE_GRAYSCALE );
  21. detector.detect( im1, keypoints1);
  22. extractor.compute( im1,keypoints1,descriptors1);
  23. std::cout<<"find "<<keypoints1.size()<<"keypoints in im1"<<std::endl;
  24. double t2 = omp_get_wtime( );
  25. std::cout<<"time: "<<t2-t1<<std::endl;
  26. matcher.match( descriptors0, descriptors1, matches );
  27. cv::Mat img_matches;
  28. cv::drawMatches( im0, keypoints0, im1, keypoints1, matches, img_matches );
  29. cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
  30. cv::imshow( "Matches", img_matches );
  31. cv::waitKey(0);
  32. return 1;
  33. }

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

[cpp] view plaincopy
  1. #include "opencv2/highgui/highgui.hpp"
  2. #include "opencv2/features2d/features2d.hpp"
  3. #include <iostream>
  4. #include <vector>
  5. #include <omp.h>
  6. int main( ){
  7. int imNum = 2;
  8. std::vector<cv::Mat> imVec(imNum);
  9. std::vector<std::vector<cv::KeyPoint>>keypointVec(imNum);
  10. std::vector<cv::Mat> descriptorsVec(imNum);
  11. cv::SurfFeatureDetector detector( 400 );    cv::SurfDescriptorExtractor extractor;
  12. cv::BruteForceMatcher<cv::L2<float> > matcher;
  13. std::vector< cv::DMatch > matches;
  14. char filename[100];
  15. double t1 = omp_get_wtime( );
  16. #pragma omp parallel for
  17. for (int i=0;i<imNum;i++){
  18. sprintf(filename,"rgb%d.jpg",i);
  19. imVec[i] = cv::imread( filename, CV_LOAD_IMAGE_GRAYSCALE );
  20. detector.detect( imVec[i], keypointVec[i] );
  21. extractor.compute( imVec[i],keypointVec[i],descriptorsVec[i]);
  22. std::cout<<"find "<<keypointVec[i].size()<<"keypoints in im"<<i<<std::endl;
  23. }
  24. double t2 = omp_get_wtime( );
  25. std::cout<<"time: "<<t2-t1<<std::endl;
  26. matcher.match( descriptorsVec[0], descriptorsVec[1], matches );
  27. cv::Mat img_matches;
  28. cv::drawMatches( imVec[0], keypointVec[0], imVec[1], keypointVec[1], matches, img_matches );
  29. cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
  30. cv::imshow( "Matches", img_matches );
  31. cv::waitKey(0);
  32. return 1;
  33. }

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

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

[cpp] view plaincopy
  1. #include "opencv2/highgui/highgui.hpp"
  2. #include "opencv2/features2d/features2d.hpp"
  3. #include <iostream>
  4. #include <omp.h>
  5. int main( ){
  6. cv::SurfFeatureDetector detector( 400 );
  7. cv::SurfDescriptorExtractor extractor;
  8. cv::BruteForceMatcher<cv::L2<float> > matcher
  9. std::vector< cv::DMatch > matches;
  10. cv::Mat im0,im1;
  11. std::vector<cv::KeyPoint> keypoints0,keypoints1;
  12. cv::Mat descriptors0, descriptors1;
  13. double t1 = omp_get_wtime( );
  14. #pragma omp parallel sections
  15. {
  16. #pragma omp section
  17. {
  18. std::cout<<"processing im0"<<std::endl;
  19. im0 = cv::imread("rgb0.jpg", CV_LOAD_IMAGE_GRAYSCALE );
  20. detector.detect( im0, keypoints0);
  21. extractor.compute( im0,keypoints0,descriptors0);
  22. std::cout<<"find "<<keypoints0.size()<<"keypoints in im0"<<std::endl;
  23. }
  24. #pragma omp section
  25. {
  26. std::cout<<"processing im1"<<std::endl;
  27. im1 = cv::imread("rgb1.jpg", CV_LOAD_IMAGE_GRAYSCALE );
  28. detector.detect( im1, keypoints1);
  29. extractor.compute( im1,keypoints1,descriptors1);
  30. std::cout<<"find "<<keypoints1.size()<<"keypoints in im1"<<std::endl;
  31. }
  32. }
  33. double t2 = omp_get_wtime( );
  34. std::cout<<"time: "<<t2-t1<<std::endl;
  35. matcher.match( descriptors0, descriptors1, matches );
  36. cv::Mat img_matches;
  37. cv::drawMatches( im0, keypoints0, im1, keypoints1, matches, img_matches );
  38. cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
  39. cv::imshow( "Matches", img_matches );
  40. cv::waitKey(0);
  41. return 1;
  42. }

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

[cpp] view plaincopy
  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://www.cnblogs.com/lexus/archive/2012/08/21/2648424.html

OpenMP的一点使用经验相关推荐

  1. openMP的一点使用经验 四

    openMP的一点使用经验 四 http://www.cnblogs.com/yangyangcv/archive/2012/03/23/2413335.html 有了以上基本知识,对我来说做很多事情 ...

  2. c++imread 函数_关于图像读取函数imread()的一点使用经验,注意默认参数的赋值

    标签: 读入数字图像到数组,用CNN进行训练,发现关于图像读取的一个问题. 问题描述:读取灰度数字图像,在验证时发现存在错误,从图像到数组中的值不完全一样? main code as follows: ...

  3. SkinMagic的一点使用经验,特此共享!

    前些天犯了些错误,就是那个程序加参数运行的问题,事实不需要那么麻烦,而且WinMain的第三个参数 也是可以用的,具体用法如下: hInstance  =  hInst;  lpCommLine  = ...

  4. OpenMP相关文章收藏学习

    BLOG 关于生产者消费者问题的OpenMP实现 https://blog.csdn.net/zhangjt33/article/details/81165253 简介:关于生产者消费者问题的Open ...

  5. OpenCV算法加速的一些学习总结

    一.概述 算法加速在实际软件层面应用来说 大数据和复杂计算的过程中 算法优化,指降低算法计算复杂度,设计新算法快速求解,比如Hungarian匹配算法.或牺牲一些内存,预计算一些重复计算的过程,减少程 ...

  6. openmp使用经验

    最近在看多核编程.简单来说,由于现在电脑CPU一般都有两个核,4核与8核的CPU也逐渐走入了寻常百姓家,传统的单线程编程方式难以发挥多核CPU的强大功能,于是多核编程应运而生.按照我的理解,多核编程可 ...

  7. 用openMP进行并行加速

    用openMP进行并行加速 参考:http://blog.csdn.net/lanbing510/article/details/17108451 最近在看多核编程.简单来说,由于现在电脑CPU一般都 ...

  8. 一个openMP编程处理图像的示例

    一个openMP编程处理图像的示例: 从硬盘读入两幅图像,对这两幅图像分别提取特征点,特征点匹配,最后将图像与匹配特征点画出来.理解该例子需要一些图像处理的基本知识,我不在此详细介绍.另外,编译该例需 ...

  9. 用OpenMP加速你的程序[转]

    最近在看多核编程.简单来说,由于现在电脑CPU一般都有两个核,4核与8核的CPU也逐渐走入了寻常百姓家,传统的单线程编程方式难以发挥多核 CPU的强大功能,于是多核编程应运而生.按照我的理解,多核编程 ...

最新文章

  1. Conversations
  2. false libhadoop was built without ISA-L support以及编译hadoop本地库失败(没有解决)
  3. php提交之前验证数据ajax提示,在通过Ajax请求提交之前使用jQuery进行表单验证
  4. 动态规划经典题之编辑距离
  5. virtualbox禁用硬件虚拟化_Mac版Virtualbox6.1开启嵌套虚拟化
  6. PyTorch学习笔记:PyTorch初体验
  7. 序列化(串行化)- 使用BinaryFormatter进行序列化
  8. LeetCode:Restore IP Addresses
  9. linux login 安装桌面,Linux_Ubuntu Linux下安装配置fluxbox桌面环境,安装   基本系统Ubuntu 7.10 G - phpStudy...
  10. dnSpy 反编译exe
  11. 电视信号服务器,基于Web服务器远程控制数字电视信号节目源再利用系统
  12. C语言练习作品 - U盘病毒模拟
  13. JQuery(js辅助开发类库)
  14. Excel函数带你看透身份证号
  15. 24V行车记录仪4Ω抛负载的选型及测试
  16. python开发cs程序_CSE209代做、代写Computer Graphics、代做CS/python编程设计代写Python程序|代做Processing...
  17. 【Git】Git下载安装与使用(一)
  18. 数据分析入门——美国各州人口分析
  19. 使用Amazon SageMaker Feature Store存储、发现并共享机器学习特征
  20. 基于STM32单片机的ILI9341液晶屏驱动protues仿真

热门文章

  1. java acm 母牛的故事_acm母牛的故事 的问题
  2. java lock condition_Java 通过 Lock 和 竞争条件 Condition 实现生产者消费者模式
  3. python将txt转换为csv_Python Pandas 三行代码将 txt 文件转换成 csv 文件
  4. 浏览器登录java_java – 如何停止已登录的用户从其他浏览器登录
  5. java byter是字节吗_GitHub - XXQAQ/Byter: 字节对象转换框架,一个基于字节的 Gson/FastJson...
  6. java可以多重继承吗_Java中的多重继承与组合vs继承
  7. 给数组倒序_22个超详细的 JS 数组方法
  8. mysql 执行计划大于_Mysql执行计划(大章)
  9. 程序员在很多人心里的作用...
  10. 阿里招“AI鉴黄体验官”:日薪1000!网友:钱不钱无所谓,净化互联网人人有责!...