[轉]openmp的一點使用經驗

最近在看多核編程。簡單來說,由於現在電腦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遍。

複製代碼
 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秒。下面我們用一句話把上面代碼變成多核運行。

複製代碼
 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核數),然後把每份指派給一個核去執行,而且多核之間爲並行執行。下面的代碼驗證了這種分析。

複製代碼
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裏。

複製代碼
 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裏,代碼如下

複製代碼
 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),見下面代碼:

複製代碼
 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裏。

複製代碼
 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的安裝與配置也不在此介紹。我們首先來看傳統串行編程的方式。

複製代碼
 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 }
複製代碼

很明顯,讀入圖像,提取特徵點與特徵描述子這部分可以改爲並行執行,修改如下:

複製代碼
 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,代碼如下:

複製代碼
 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 ); cv::SurfDescriptorExtractor extractor;
7 cv::BruteForceMatcher<cv::L2<float> > matcher;
8 std::vector< cv::DMatch > matches;
9 cv::Mat im0,im1;
10 std::vector<cv::KeyPoint> keypoints0,keypoints1;
11 cv::Mat descriptors0, descriptors1;
12 double t1 = omp_get_wtime( );
13 #pragma omp parallel sections
14 {
15 #pragma omp section
16 {
17 std::cout<<"processing im0"<<std::endl;
18 im0 = cv::imread("rgb0.jpg", CV_LOAD_IMAGE_GRAYSCALE );
19 detector.detect( im0, keypoints0);
20 extractor.compute( im0,keypoints0,descriptors0);
21 std::cout<<"find "<<keypoints0.size()<<"keypoints in im0"<<std::endl;
22 }
23 #pragma omp section
24 {
25 std::cout<<"processing im1"<<std::endl;
26 im1 = cv::imread("rgb1.jpg", CV_LOAD_IMAGE_GRAYSCALE );
27 detector.detect( im1, keypoints1);
28 extractor.compute( im1,keypoints1,descriptors1);
29 std::cout<<"find "<<keypoints1.size()<<"keypoints in im1"<<std::endl;
30 }
31 }
32 double t2 = omp_get_wtime( );
33 std::cout<<"time: "<<t2-t1<<std::endl;
34 matcher.match( descriptors0, descriptors1, matches );
35 cv::Mat img_matches;
36 cv::drawMatches( im0, keypoints0, im1, keypoints1, matches, img_matches );
37 cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
38 cv::imshow( "Matches", img_matches );
39 cv::waitKey(0);
40 return 1;
41 }
複製代碼

上面代碼中,我們首先用#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

附加兩個自己找到的相關文章:

http://berenger.eu/blog/c-openmp-examples-of-basic-parallel-programming/

http://software.intel.com/zh-cn/articles/more-work-sharing-with-openmp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章