OpenMP的一点使用经验
来源:互联网 发布:windows pagefile.sys 编辑:程序博客网 时间:2024/06/16 01:36
这个学期学习了朱利老师讲的计算机系统结构,在第十章里讲解的就是多核编程,在这一章留了一个大作业,要求用到多核编程。由来就是这样,下面就对多核编程在应用方面做一个简单的介绍。
简单来说,由于现在电脑CPU一般都有四个核,8核的CPU的使用也逐渐越来越广泛,传统的单线程编程方式难以发挥多核CPU的强大功能,于是多核编程应运而生。按照我的理解,多核编程可以认为是对多线程编程做了一定程度的抽象,提供一些简单的API,使得用户不必花费太多精力来了解多线程的底层知识,从而提高编程效率。
目录
- 目录
- 基本概念
- 使用配置
- 初步理解
- 竞态条件race condition
- 归约reduction
- critical
- 扩展知识
- 目录
基本概念
这两天关注的多核编程的工具包括openMP和TBB。按照目前网上的论,TBB风头要盖过openMP,比如openCV过去是使用openMP的,但从2.3版本开始抛弃openMP,转向TBB。TBB还是比较复杂的,相比之下,openMP则非常容易上手。因为精力和时间有限,没办法花费太多时间去学习TBB,就在这里分享下这两天学到的openMP的一点知识,和大家共同讨论。
openMP是一个针对共享内存架构的多线程编程标准,是一个基于显式编译指导,与平台无关、并行程序设计的API,用于编写可移植的多线程应用程序。支持的编程语言包括C语言、C++和Fortran,支持OpenMP的编译器包括Sun Studio,Intel Compiler,Microsoft Visual Studio,GCC,支持大多数的类UNIX和Windows操作系统。
openMP程序设计模型提供了提供了一组与平台无关的编译指导(Pragmas)、指导命令(Directive)、函数调用和环境变量,显式地指导编译器开发程序中的并行性。对于很多的循环来说,都可以在循环体之前插入一条编译指导语句,使其以多线程执行。程序设计人员无需关心如何实现并行、如何同步等细节,这是编译器和OpenMP线程库的工作。程序设计人员需要认真分析哪些循环应该并行执行、如何重构算法以便适应多核架构等问题即可。
使用配置
在实验中我使用的是Microsoft Visual Studio 2015,CPU为Intel core i5 四核,首先讲一下在Microsoft Visual Studio 2015上openMP的配置。非常简单,总共分2步:
(1) 新建一个工程。这个不再多讲。
(2) 建立工程后,点击 菜单栏->Project->Properties,弹出菜单里,点击 Configuration Properties->C/C++->Language->OpenMP Support,在下拉菜单里选择Yes。
至此配置结束。
初步理解
下面我们通过一个小例子来说明openMP的易用性。这个例子是 有一个简单的test()函数,然后在main()里,用一个for循环把这个test()函数跑8遍。
这里是没有使用openMP执行的代码
#include <iostream>#include <time.h>void test(){ int a = 0; for (int i = 0; i<100000000; i++) a++;}int main(){ clock_t t1 = clock(); for (int i = 0; i<8; i++) test(); clock_t t2 = clock(); std::cout << "time: " << t2 - t1 << std::endl; return 0;}
#include <iostream>#include <time.h>void test(){ int a = 0; for (int i = 0; i<100000000; i++) a++;}int main(){ clock_t t1 = clock(); #pragma omp parallel for for (int i = 0; i<8; i++) test(); clock_t t2 = clock(); std::cout << "time: " << t2 - t1 << std::endl; }
由此我们可以看到openMP的简单易用。在上面的代码里,我们一没有额外include头文件,二没有额外link库文件,只是在for循环前加了一句#pragma omp parallel for。而且这段代码在单核机器上,或者编译器没有将openMP设为Yes的机器上编译也不会报错,将自动忽略#pragma这行代码,然后按照传统单核串行的方式编译运行!
对上面代码按照我的理解做个简单的剖析。
当编译器发现#pragma omp parallel for后,自动将下面的for循环分成N份,(N为电脑CPU核数),然后把每份指派给一个核去执行,而且多核之间为并行执行。下面我们通过一段代码进行验证。
#include <iostream>using namespace std;int main(){ #pragma omp parallel for for (int i = 0; i<10; i++) cout << i <<" "; cout << endl; return 0; }
按照串行程序执行,我们得到的结果应该是0-9
注意:因为每个核之间是并行执行,所以每次执行时打印出的顺序可能都是不一样的。
竞态条件(race condition)
这是所有多线程编程最棘手的问题。该问题可表述为,当多个线程并行执行时,有可能多个线程同时对某变量进行了读写操作,从而导致不可预知的结果。比如下面的例子,对于包含10个整型元素的数组a,我们用for循环求它各元素之和,并将结果保存在变量sum里。
#include<iostream>int validate(){ int sum = 0; int a[10] = { 1,2,3,4,5,6,7,8,9,10 }; #pragma omp parallel for for (int i = 0; i < 10; i++) { sum += a[i]; } return sum;}int main(){ int sum=validate(); while (sum == 55) { sum = validate(); } std::cout << "sum:" << sum << std::endl; return 0;}
如果我们注释掉#pragma omp parallel for,让程序先按照传统串行的方式执行,程序永远不会结束。很明显,sum 永远为 55。但按照并行方式执行后,sum则会变成其他值,比如在某次运行过程中,sum = 40。其原因是,当某线程A执行sum = sum + a[i]的同时,另一线程B正好在更新sum,而此时A还在用旧的sum做累加,于是出现了错误。
那么用openMP怎么实现并行数组求和呢?下面我们先给出一个基本的解决方案。该方案的思想是,首先生成一个数组sumArray,其长度为并行执行的线程的个数(默认情况下,该个数等于CPU的核数),在for循环里,让各个线程更新自己线程对应的sumArray里的元素,最后再将sumArray里的元素累加到sum里,代码如下
#include <iostream>#include <omp.h>int main(){ int sum = 0; int a[10] = { 1,2,3,4,5,6,7,8,9,10 }; int coreNum = omp_get_num_procs();//获得处理器的个数 int *sumArray = new int[coreNum];//对应于处理器的个数生成一个数组 for (size_t i = 0; i < coreNum; i++)//将数组的每个元素初始化为0 { sumArray[i] = 0; } #pragma omp parallel for for (int i = 0; i < 10; i++)//在OpenMP语句中的索引变量必须是有符号的整型 { int k = omp_get_thread_num();//获得每个线程的ID sumArray[k] = sumArray[k] + a[i]; } for (size_t i = 0; i < coreNum; i++) { sum += sumArray[i]; } std::cout << "sum:" << sum << std::endl; return 0;}
需要注意的是,在上面代码里,我们用omp_get_num_procs()函数来获取处理器个数,用omp_get_thread_num()函数来获得每个线程的ID,为了使用这两个函数,我们需要include
归约(reduction)
上面的代码虽然达到了目的,但它产生了较多的额外操作,比如要先生成数组sumArray,最后还要用一个for循环将它的各元素累加起来,有没有更简便的方式呢?答案是有,openMP为我们提供了另一个工具,归约(reduction),见下面代码:
#include<iostream>int main(){ int sum = 0; int a[10] = { 1,2,3,4,5,6,7,8,9,10 }; #pragma omp parallel for reduction(+:sum) for (int i = 0; i < 10; i++) { sum += a[i]; } std::cout << "sum:" << sum << std::endl; return 0;}
上面代码里,我们在#pragma omp parallel for 后面加上了 reduction(+:sum),它的意思是告诉编译器:下面的for循环你要分成多个线程跑,但每个线程都要保存变量sum的拷贝,循环结束后,所有线程把自己的sum累加起来作为最后的输出。
critical
reduction虽然很方便,但它只支持一些基本操作,比如+,-,*,&,|,&&,||等。有些情况下,我们既要避免race condition,但涉及到的操作又超出了reduction的能力范围,应该怎么办呢?这就要用到openMP的另一个工具,critical。来看下面的例子,该例中我们求数组a的最大值,将结果保存在max里。
#include <iostream>int main() { int max = 0; int a[10] = { 11,2,33,49,113,20,321,250,689,16 }; #pragma omp parallel for for (int i = 0; i<10; i++) { int temp = a[i]; #pragma omp critical { if (temp > max) max = temp; } } std::cout << "max: " << max << std::endl; return 0;}
上例中,for循环还是被自动分成N份来并行执行,但我们用#pragma omp critical将 if (temp > max) max = temp 括了起来,它的意思是:各个线程还是并行执行for里面的语句,但当你们执行到critical里面时,要注意有没有其他线程正在里面执行,如果有的话,要等其他线程执行完再进去执行。这样就避免了race condition问题,但显而易见,它的执行速度会变低,因为可能存在线程等待的情况。
扩展知识
有了以上基本知识,对我们来说做很多事情都足够了。下面我们来看一个具体的应用例,从硬盘读入两幅图像,对这两幅图像分别提取特征点,特征点匹配,最后将图像与匹配特征点画出来。理解该例子需要一些图像处理的基本知识,我不在此详细介绍。另外,编译该例需要opencv,我用的版本是3.2.0,因为没有添加opencv_contrib模块,所以没有再次测试,关于opencv的安装与配置也不在此介绍,可以参考OpenCV3.20 在VS2015中的配置,关于扩展模块opencv_contrib的安装可以参考这里。我们首先来看传统串行编程的方式。
#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 <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,每个里面执行了图像读取、特征点与特征描述子提取。将其简化为伪代码形式即为:
#pragma omp parallel sections{ #pragma omp section { function1(); } #pragma omp section { function2(); }}
意思是:parallel sections里面的内容要并行执行,具体分工上,每个线程执行其中的一个section,如果section数大于线程数,那么就等某线程执行完它的section后,再继续执行剩下的section。在时间上,这种方式与人为用vector构造for循环的方式差不多,但无疑该种方式更方便,而且在单核机器上或没有开启openMP的编译器上,该种方式不需任何改动即可正确编译,并按照单核串行方式执行。
以上分享了这两天关于OpenMP的一点学习体会,其中难免有错误,欢迎指正。
在写上文的过程中,参考了包括以下两个网址在内的多个地方的资源,不再一 一列出,在此一并表示感谢。
http://blog.csdn.net/drzhouweiming/article/details/4093624
http://software.intel.com/zh-cn/articles/more-work-sharing-with-openmp
http://www.cnblogs.com/yangyangcv/archive/2012/03/23/2413335.html
- OpenMP: OpenMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- openMP的一点使用经验
- OpenMP的一点使用经验
- OpenMP的一点使用经验
- 动、静态网页服务器软件
- Linux Centos环境下通过LuaSQL访问Oracle数据库
- java基本程序设计 ------java基础
- Prim算法 数组实现
- IO基本的字节字符
- OpenMP的一点使用经验
- 数值型全讲解
- JMX中MBean实现
- HDOJ 2153 仙人球的残影(水题)
- node学习笔记(一)
- [笔记]kubantu安装pip、tensorflow和keras
- 拥塞控制分析之BIC与CUBIC
- Recycler的适配器和holder
- 提高MD5安全性