1. 程式人生 > >OpenMp實現並行化

OpenMp實現並行化

隱式 必須 board chunk 並行化 ads 任務 方法 sso

前言

昨天,往arm上移植opencv程序,發現運行速度很慢。觀察資源監視器發現只有一個核處於高負荷(總共4核),遂考慮到需要多核計算。OpenMp和MPI是常用並行計算庫,OpenMP相對簡單適合單機多核多線程,MPI適合集群,但復雜。

OpenMp是由OpenMP Architecture Review Board牽頭提出的,並已被廣泛接受的,用於共享內存並行系統的多處理器程序設計的一套指導性的編譯處理方案(Compiler Directive)。OpenMP支持的編程語言包括C語言、C++和Fortran;而支持OpenMp的編譯器包括Sun Compiler,GNU Compiler和Intel Compiler等。OpenMp提供了對並行算法的高層的抽象描述,程序員通過在源代碼中加入專用的pragma來指明自己的意圖,由此編譯器可以自動將程序進行並行化,並在必要之處加入同步互斥以及通信。當選擇忽略這些pragma,或者編譯器不支持OpenMp時,程序又可退化為通常的程序(一般為串行),代碼仍然可以正常運作,只是不能利用多線程來加速程序執行。

使用

基本使用很簡單。G++編譯時加上-fopenmp

以下為引用:

1.
OpenMP是一種API,用於編寫可移植的多線程應用程序,無需程序員進行復雜的線程創建、同步、負載平衡和銷毀工作。
使用OpenMP的好處:
1)CPU核數擴展性問題
2)方便性問題
3)可移植性問題
OpenMP指令和庫函數介紹:
在C/C++中,OpenMP指令使用的格式為:#pragma omp 指令 [子句[子句]…]
用法詳見OpenMP編程指南。
2.
#pragma omp parallel for
for(i=0;i<length;i++)
{
//沒有循環叠代相關的語句,如把圖像數組中的RGB值轉為灰度值。
}
3.
對可以以多線程執行的循環的約束:
1)循環變量必須是有符號整型,如果是無符號整型,就無法使用
2)比較操作必須是<,>,<=,>=
3)循環步長必須是整數加或整數減操作,加減的操作必須是一個不變量
4)如果是<,<=,循環變量的值每次叠代時必須增加,否則減小
5)循環內部不允許有能夠到達循環之外的跳轉語句,也不允許有外部的跳轉語句到達循環內部。exit語句例外,goto 和break的跳轉範圍必須在循環內部,異常處理也必須在循環內部處理
4.
數據相關(以下假設為語句S2與語句S1存在數據相關):
相關的種類(相關不等於循環叠代相關):
1)流相關:S1先寫某一存儲單元,而後S2又讀該單元
2)輸出相關:兩個語句寫同一存儲單元
3)反相關:一個語句先讀一單元,然後另一語句寫該單元
相關產生的方式:
1)S1在循環的一次叠代中訪問存儲單元L,S2在隨後的一次叠代中訪問L(是循環叠代相關)
2)S1和S2在同一循環叠代中訪問同一存儲單元L,但S1的執行在S2之前。(非循環叠代相關)
5.
數據競爭:
數據競爭可能是由於輸出相關引起的,編譯器不會進行數據競爭的檢測,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);//返回可用的處理核個數
16.
編譯OpenMP要需要一個支持OpenMP的編譯器和線程安全的運行時庫。vs2005的配置屬性C/C++語言裏提供對OpenMP的支持。
編譯時假如出現"沒有找到vcompd.dll,因此這個應用程序未能啟動。重新安裝應用程序可能會修復此問題",
可能的原因是該項目有可能是從VC移植過來的,如果由VS創建,一般不會出現該問題,因為VS會解決在清單文件的調用dll問題。
解決方法如下:
StdAfx.h中加入 #pragma comment(linker, "\"/manifestdependency:type=‘Win32‘ name=‘Microsoft.VC80.DebugOpenMP‘ version=‘8.0.50608.0‘ processorArchitecture=‘X86‘ publicKeyToken=‘1fc8b3b9a1e18e3b‘ language=‘*‘\"")
或者在Linker -> Manifest File -> Additional Manifest Dependencies -> 中加入:
"type=‘Win32‘ name=‘Microsoft.VC80.DebugOpenMP‘ version=‘8.0.50608.0‘ processorArchitecture=‘X86‘ publicKeyToken=‘1fc8b3b9a1e18e3b‘ language=‘*‘"

資料

http://www.openmp.org/

OpenMp實現並行化