OpenMP: OpenMP编程指南
????????? 進入多核時代后,必須使用多線程編寫程序才能讓各個CPU核得到利用。在單核時代,通常使用操作系統提供的API來創建線程,然而,在多核系統中,情況發生了很大的變化, 如果仍然使用操作系統API來創建線程會遇到一些問題。具體來說,有以下三個問題:
1)CPU核數擴展性問題
????????? 多核編程需要考慮程序性能隨CPU核數的擴展性,即硬件升級到更多核后,能夠不修改程序就讓程序性能增長,這要求程序中創建的線程數量需要隨CPU核數變化,不能創建固定數量的線程,否則在CPU核數超過線程數量上的機器上運行,將無法完全利用機器性能。雖然通過一定方法可以使用操作系統API創建可變化數量的線程,但是比較麻煩,不如OpenMP方便。
2)方便性問題
????????? 在多核編程時,要求計算均攤到各個CPU核上去,所有的程序都需要并行化執行,對計算的負載均衡有很高要求。這就要求在同一個函數內或同一個循環中,可能也需要將計算分攤到各個CPU核上,需要創建多個線程。操作系統API創建線程時,需要線程入口函數,很難滿足這個需求,除非將一個函數內的代碼手工拆成多個線程入口函數,這將大大增加程序員的工作量。使用OpenMP創建線程則不需要入口函數,非常方便,可以將同一函數內的代碼分解成多個線程執行,也可以將一個for循環分解成多個線程執行。
3)可移植性問題
???????? 目前各個主流操作系統的線程API互不兼容,缺乏事實上的統一規范,要滿足可移植性得自己寫一些代碼,將各種不同操作系統的api封裝成一套統一的接口。OpenMP是標準規范,所有支持它的編譯器都是執行同一套標準,不存在可移植性問題。
綜上所述,在多核編程中,使用OpenMP就很有必要,下面列出以前發表在我的CSDN博客中的OpenMP文章,供大家參考。
OpenMP并行程序設計(一) OpenMP是一個支持共享存儲并行設計的庫,特別適宜多核CPU上的并行程序設計。今天在雙核CPU機器上試了一下OpenMP并行程序設計,發現效率方面超出想象,因此寫出來分享給大家。 在VC8.0中項目的屬性對話框中,左邊框里的“配置屬性”下的“C/C++”下的“語言”頁里,將OpenMP支持改為“是/(OpenMP)”就可以支持OpenMP了。 先看一個簡單的使用了OpenMP程序 int main(int argc,char* argv[]) { #pragma omp parallelfor ???? for (int i = 0; i < 10; i++ ) ???? { ???????? printf("i = %d/n", i); ???? } ???? return 0; } 這個程序執行后打印出以下結果: i = 0 i = 5 i = 1 i = 6 i = 2 i = 7 i = 3 i = 8 i = 4 i = 9 可見for 循環語句中的內容被并行執行了。(每次運行的打印結果可能會有區別) 這里要說明一下,#pragma omp parallelfor這條語句是用來指定后面的for循環語句變成并行執行的,當然for循環里的內容必須滿足可以并行執行,即每次循環互不相干,后一次循環不依賴于前面的循環。 有關#pragma omp parallelfor這條語句的具體含義及相關OpenMP指令和函數的介紹暫時先放一放,只要知道這條語句會將后面的for循環里的內容變成并行執行就行了。 ?將for循環里的語句變成并行執行后效率會不會提高呢,我想這是我們最關心的內容了。下面就寫一個簡單的測試程序來測試一下: void test() { ???? int a = 0; ???? clock_t t1 = clock(); ???? for (int i = 0; i < 100000000; i++) ???? { ???????? a = i+1; ???? } ???? clock_t t2 = clock(); ???? printf("Time = %d/n", t2-t1); } int main(int argc,char* argv[]) { ???? clock_t t1 = clock(); #pragma omp parallelfor ???? for (int j = 0; j < 2; j++ ){ ???????? test(); ???? } ???? clock_t t2 = clock(); ???? printf("Total time = %d/n", t2-t1); ???? test(); ???? return 0; } 在test()函數中,執行了1億次循環,主要是用來執行一個長時間的操作。 在main()函數里,先在一個循環里調用test()函數,只循環2次,我們還是看一下在雙核CPU上的運行結果吧: Time = 297 Time = 297 Total time = 297 Time = 297 可以看到在for循環里的兩次test()函數調用都花費了297ms, 但是打印出的總時間卻只花費了297ms,后面那個單獨執行的test()函數花費的時間也是297ms,可見使用并行計算后效率提高了整整一倍。
OpenMP并行程序設計(二) 1、fork/join并行執行模式的概念 OpenMP是一個編譯器指令和庫函數的集合,主要是為共享式存儲計算機上的并行程序設計使用的。 前面一篇文章中已經試用了OpenMP的一個Parallel for指令。從上篇文章中我們也可以發現OpenMP并行執行的程序要全部結束后才能執行后面的非并行部分的代碼。這就是標準的并行模式fork/join式并行模式,共享存儲式并行程序就是使用fork/join式并行的。 標準并行模式執行代碼的基本思想是,程序開始時只有一個主線程,程序中的串行部分都由主線程執行,并行的部分是通過派生其他線程來執行,但是如果并行部分沒有結束時是不會執行串行部分的,如上一篇文章中的以下代碼: int main(int argc,char* argv[]) { ???? clock_t t1 = clock(); #pragma omp parallelfor ???? for ( int j = 0; j < 2; j++ ){ ???????? test(); ???? } ???? clock_t t2 = clock(); ???? printf("Total time = %d/n", t2-t1); ???? test(); ???? return 0; } 在沒有執行完for循環中的代碼之前,后面的clock_t t2 = clock();這行代碼是不會執行的,如果和調用線程創建函數相比,它相當于先創建線程,并等待線程執行完,所以這種并行模式中在主線程里創建的線程并沒有和主線程并行運行。 2、OpenMP指令和庫函數介紹 下面來介紹OpenMP的基本指令和常用指令的用法, 在C/C++中,OpenMP指令使用的格式為 ??????#pragma omp指令 [子句[子句]…] 前面提到的parallel for就是一條指令,有些書中也將OpenMP的“指令”叫做“編譯指導語句”,后面的子句是可選的。例如: #pragma omp parallel private(i, j) parallel 就是指令, private是子句 為敘述方便把包含#pragma和OpenMP指令的一行叫做語句,如上面那行叫parallel語句。 OpenMP的指令有以下一些: parallel,用在一個代碼段之前,表示這段代碼將被多個線程并行執行 for,用于for循環之前,將循環分配到多個線程中并行執行,必須保證每次循環之間無相關性。 parallel for, parallel 和 for語句的結合,也是用在一個for循環之前,表示for循環的代碼將被多個線程并行執行。 sections,用在可能會被并行執行的代碼段之前 parallel sections,parallel和sections兩個語句的結合 critical,用在一段代碼臨界區之前 single,用在一段只被單個線程執行的代碼段之前,表示后面的代碼段將被單線程執行。 flush, barrier,用于并行區內代碼的線程同步,所有線程執行到barrier時要停止,直到所有線程都執行到barrier時才繼續往下執行。 atomic,用于指定一塊內存區域被制動更新 master,用于指定一段代碼塊由主線程執行 ordered, 用于指定并行區域的循環按順序執行 threadprivate, 用于指定一個變量是線程私有的。 OpenMP除上述指令外,還有一些庫函數,下面列出幾個常用的庫函數: omp_get_num_procs, 返回運行本線程的多處理機的處理器個數。 omp_get_num_threads, 返回當前并行區域中的活動線程個數。 omp_get_thread_num,?返回線程號 omp_set_num_threads,?設置并行執行代碼時的線程個數 omp_init_lock, 初始化一個簡單鎖 omp_set_lock, 上鎖操作 omp_unset_lock, 解鎖操作,要和omp_set_lock函數配對使用。 omp_destroy_lock, omp_init_lock函數的配對操作函數,關閉一個鎖 OpenMP的子句有以下一些 private,指定每個線程都有它自己的變量私有副本。 firstprivate,指定每個線程都有它自己的變量私有副本,并且變量要被繼承主線程中的初值。 lastprivate,主要是用來指定將線程中的私有變量的值在并行處理結束后復制回主線程中的對應變量。 reduce,用來指定一個或多個變量是私有的,并且在并行處理結束后這些變量要執行指定的運算。 nowait,忽略指定中暗含的等待 num_threads,指定線程的個數 schedule,指定如何調度for循環迭代 shared,指定一個或多個變量為多個線程間的共享變量 ordered,用來指定for循環的執行要按順序執行 copyprivate,用于single指令中的指定變量為多個線程的共享變量 copyin,用來指定一個threadprivate的變量的值要用主線程的值進行初始化。 default,用來指定并行處理區域內的變量的使用方式,缺省是shared 3、parallel指令的用法 parallel 是用來構造一個并行塊的,也可以使用其他指令如for、sections等和它配合使用。 在C/C++中,parallel的使用方法如下: #pragma omp parallel [for | sections] [子句[子句]…] { //代碼 } parallel語句后面要跟一個大括號對將要并行執行的代碼括起來。 void main(int argc, char *argv[]) { #pragma omp parallel { printf(“Hello, World!/n”); } } 執行以上代碼將會打印出以下結果 Hello, World! Hello, World! Hello, World! Hello, World! 可以看得出parallel語句中的代碼被執行了四次,說明總共創建了4個線程去執行parallel語句中的代碼。 也可以指定使用多少個線程來執行,需要使用num_threads子句: void main(int argc, char *argv[]) { #pragma omp parallel num_threads(8) { printf(“Hello, World!, ThreadId=%d/n”, omp_get_thread_num() ); } } 執行以上代碼,將會打印出以下結果: Hello, World!, ThreadId = 2 Hello, World!, ThreadId = 6 Hello, World!, ThreadId = 4 Hello, World!, ThreadId = 0 Hello, World!, ThreadId = 5 Hello, World!, ThreadId = 7 Hello, World!, ThreadId = 1 Hello, World!, ThreadId = 3 從ThreadId的不同可以看出創建了8個線程來執行以上代碼。所以parallel指令是用來為一段代碼創建多個線程來執行它的。parallel塊中的每行代碼都被多個線程重復執行。 和傳統的創建線程函數比起來,相當于為一個線程入口函數重復調用創建線程函數來創建線程并等待線程執行完。 4、for指令的使用方法 for指令則是用來將一個for循環分配到多個線程中執行。for指令一般可以和parallel指令合起來形成parallel for指令使用,也可以單獨用在parallel語句的并行塊中。 #pragma omp [parallel] for [子句] ????? for循環語句 先看看單獨使用for語句時是什么效果: int j = 0; #pragma ompfor ???? for ( j = 0; j < 4; j++ ){ ???????? printf(“j = %d, ThreadId = %d/n”, j, omp_get_thread_num()); ???? } 執行以上代碼后打印出以下結果 j = 0, ThreadId = 0 j = 1, ThreadId = 0 j = 2, ThreadId = 0 j = 3, ThreadId = 0 從結果可以看出四次循環都在一個線程里執行,可見for指令要和parallel指令結合起來使用才有效果: 如以下代碼就是parallel 和for一起結合成parallel for的形式使用的: int j = 0; #pragma omp parallelfor ???? for ( j = 0; j < 4; j++ ){ ???????? printf(“j = %d, ThreadId = %d/n”, j, omp_get_thread_num()); ???? } 執行后會打印出以下結果: j = 0, ThreadId = 0 j = 2, ThreadId = 2 j = 1, ThreadId = 1 j = 3, ThreadId = 3 可見循環被分配到四個不同的線程中執行。 上面這段代碼也可以改寫成以下形式: int j = 0; #pragma omp parallel { #pragma omp for ???? for ( j = 0; j < 4; j++ ){ ???????? printf(“j = %d, ThreadId = %d/n”, j, omp_get_thread_num()); ???? } } 執行以上代碼會打印出以下結果: j = 1, ThreadId = 1 j = 3, ThreadId = 3 j = 2, ThreadId = 2 j = 0, ThreadId = 0 在一個parallel 塊中也可以有多個for語句,如: int j; #pragma omp parallel { #pragma omp for ???? for ( j = 0; j < 100; j++ ){ ????????… ???? } #pragma omp for ???? for ( ?j = 0; j < 100; j++ ){ ????????… ???? } … } for 循環語句中,書寫是需要按照一定規范來寫才可以的,即for循環小括號內的語句要按照一定的規范進行書寫,for語句小括號里共有三條語句 for( i=start; i < end; i++) i=start; 是for循環里的第一條語句,必須寫成 “變量=初值” 的方式。如 i=0 i < end;是for循環里的第二條語句,這個語句里可以寫成以下4種形式之一: 變量 < 邊界值 變量 <= 邊界值 變量 > 邊界值 變量 >= 邊界值 如 i>10?i< 10i>=10?i>10 等等 最后一條語句i++可以有以下9種寫法之一
i++ ++i i-- --i i += inc i -= inc i = i + inc i = inc + i i = i –inc 例如i += 2;?i -= 2;i = i + 2;i = i - 2;都是符合規范的寫法。 5 sections和section指令的用法 section語句是用在sections語句里用來將sections語句里的代碼劃分成幾個不同的段,每段都并行執行。用法如下: #pragma omp [parallel] sections [子句] { ?? #pragma omp section ?? { ??????????? 代碼塊 ?? }? } 先看一下以下的例子代碼: void main(int argc, char *argv) { #pragma omp parallel sections { #pragma omp section printf(“section 1 ThreadId = %d/n”, omp_get_thread_num()); #pragma omp section printf(“section 2 ThreadId = %d/n”, omp_get_thread_num()); #pragma omp section printf(“section 3 ThreadId = %d/n”, omp_get_thread_num()); #pragma omp section printf(“section 4 ThreadId = %d/n”, omp_get_thread_num()); } 執行后將打印出以下結果: section 1 ThreadId = 0 section 2 ThreadId = 2 section 4 ThreadId = 3 section 3 ThreadId = 1 從結果中可以發現第4段代碼執行比第3段代碼早,說明各個section里的代碼都是并行執行的,并且各個section被分配到不同的線程執行。 使用section語句時,需要注意的是這種方式需要保證各個section里的代碼執行時間相差不大,否則某個section執行時間比其他section過長就達不到并行執行的效果了。 上面的代碼也可以改寫成以下形式: void main(int argc, char *argv) { #pragma omp parallel { #pragma omp sections { #pragma omp section printf(“section 1 ThreadId = %d/n”, omp_get_thread_num()); #pragma omp section printf(“section 2 ThreadId = %d/n”, omp_get_thread_num()); } #pragma omp sections { #pragma omp section printf(“section 3 ThreadId = %d/n”, omp_get_thread_num()); #pragma omp section printf(“section 4 ThreadId = %d/n”, omp_get_thread_num()); } } 執行后將打印出以下結果: section 1 ThreadId = 0 section 2 ThreadId = 3 section 3 ThreadId = 3 section 4 ThreadId = 1 這種方式和前面那種方式的區別是,兩個sections語句是串行執行的,即第二個sections語句里的代碼要等第一個sections語句里的代碼執行完后才能執行。 用for語句來分攤是由系統自動進行,只要每次循環間沒有時間上的差距,那么分攤是很均勻的,使用section來劃分線程是一種手工劃分線程的方式,最終并行性的好壞得依賴于程序員。 本篇文章中講的幾個OpenMP指令parallel, for, sections, section實際上都是用來如何創建線程的,這種創建線程的方式比起傳統調用創建線程函數創建線程要更方便,并且更高效。 當然,創建線程后,線程里的變量是共享的還是其他方式,主線程中定義的變量到了并行塊內后還是和傳統創建線程那種方式一樣的嗎?創建的線程是如何調度的?等等諸如此類的問題到下一篇文章中進行講解。
3、OpenMP中的數據處理子句????? (OpenMP程序設計的細節)
OpenMP中的數據處理子句 相關文檔連接: 多核編程中的任務隨機競爭模式的概率分析 多核編程中的任務分組競爭模式? ?多核編程中的負載平衡難題 多核編程中的鎖競爭難題 多核編程的幾個難題及其應對策略(難題一) OpenMP并行程序設計(二) ?OpenMP并行程序設計(一) ?雙核CPU上的快速排序效率? OpenMP創建線程中的鎖及原子操作性能比較 10.1.1?private子句 private子句用于將一個或多個變量聲明成線程私有的變量,變量聲明成私有變量后,指定每個線程都有它自己的變量私有副本,其他線程無法訪問私有副本。即使在并行區域外有同名的共享變量,共享變量在并行區域內不起任何作用,并且并行區域內不會操作到外面的共享變量。 private子句的用法格式如下: private(list) 下面便是一個使用private子句的代碼例子: int k = 100; #pragma omp parallel for private(k) for ( k=0; k < 10; k++) { printf("k=%d/n", k); } printf("last k=%d/n", k); 上面程序執行后打印的結果如下: k=6 k=7 k=8 k=9 k=0 k=1 k=2 k=3 k=4 k=5 last k=100 從打印結果可以看出,for循環前的變量k和循環區域內的變量k其實是兩個不同的變量。 用private子句聲明的私有變量的初始值在并行區域的入口處是未定義的,它并不會繼承同名共享變量的值。 出現在reduction子句中的參數不能出現在private子句中。 10.1.2?firstprivate子句 private聲明的私有變量不能繼承同名變量的值,但實際情況中有時需要繼承原有共享變量的值,OpenMP提供了firstprivate子句來實現這個功能。 先看一下以下的代碼例子 int k = 100; #pragma omp parallel for firstprivate(k) for ( i=0; i < 4; i++) { k+=i; printf("k=%d/n",k); } printf("last k=%d/n", k); 上面代碼執行后打印結果如下: k=100 k=101 k=103 k=102 last k=100 從打印結果可以看出,并行區域內的私有變量k繼承了外面共享變量k的值100作為初始值,并且在退出并行區域后,共享變量k的值保持為100未變。 10.1.3?lastprivate子句 有時在并行區域內的私有變量的值經過計算后,在退出并行區域時,需要將它的值賦給同名的共享變量,前面的private和firstprivate子句在退出并行區域時都沒有將私有變量的最后取值賦給對應的共享變量,lastprivate子句就是用來實現在退出并行區域時將私有變量的值賦給共享變量。 舉個例子如下: int k = 100; #pragma omp parallel for firstprivate(k),lastprivate(k) for ( i=0; i < 4; i++) { k+=i; printf("k=%d/n",k); } printf("last k=%d/n", k); 上面代碼執行后的打印結果如下: k=100 k=101 k=103 k=102 last k=103 從打印結果可以看出,退出for循環的并行區域后,共享變量k的值變成了103,而不是保持原來的100不變。 由于在并行區域內是多個線程并行執行的,最后到底是將那個線程的最終計算結果賦給了對應的共享變量呢?OpenMP規范中指出,如果是循環迭代,那么是將最后一次循環迭代中的值賦給對應的共享變量;如果是section構造,那么是最后一個section語句中的值賦給對應的共享變量。注意這里說的最后一個section是指程序語法上的最后一個,而不是實際運行時的最后一個運行完的。 如果是類(class)類型的變量使用在lastprivate參數中,那么使用時有些限制,需要一個可訪問的,明確的缺省構造函數,除非變量也被使用作為firstprivate子句的參數;還需要一個拷貝賦值操作符,并且這個拷貝賦值操作符對于不同對象的操作順序是未指定的,依賴于編譯器的定義。 10.1.4?threadprivate子句 threadprivate子句用來指定全局的對象被各個線程各自復制了一個私有的拷貝,即各個線程具有各自私有的全局對象。 用法如下: #pragma omp threadprivate(list)new-line 下面用threadprivate命令來實現一個各個線程私有的計數器,各個線程使用同一個函數來實現自己的計數。計數器代碼如下: int counter = 0; #pragma omp threadprivate(counter) int increment_counter() { counter++; return(counter); } 如果對于靜態變量也同樣可以使用threadprivate聲明成線程私有的,上面的counter變量如改成用static類型來實現時,代碼如下: int increment_counter2() { static int counter = 0; #pragma omp threadprivate(counter) counter++; return(counter); } threadprivate和private的區別在于threadprivate聲明的變量通常是全局范圍內有效的,而private聲明的變量只在它所屬的并行構造中有效。 threadprivate的對應只能用于copyin,copyprivate,schedule,num_threads和if子句中,不能用于任何其他子句中。 用作threadprivate的變量的地址不能是常數。 對于C++的類(class)類型變量,用作threadprivate的參數時有些限制,當定義時帶有外部初始化時,必須具有明確的拷貝構造函數。 對于windows系統,threadprivate不能用于動態裝載(使用LoadLibrary裝載)的DLL中,可以用于靜態裝載的DLL中,關于windows系統中的更多限制,請參閱MSDN中有關threadprivate子句的幫助材料。 有關threadprivate命令的更多限制方面的信息,詳情請參閱OpenMP2.5規范。 10.1.5?shared子句 shared子句用來聲明一個或多個變量是共享變量。 用法如下: shared(list) 需要注意的是,在并行區域內使用共享變量時,如果存在寫操作,必須對共享變量加以保護,否則不要輕易使用共享變量,盡量將共享變量的訪問轉化為私有變量的訪問。 循環迭代變量在循環構造區域里是私有的。聲明在循環構造區域內的自動變量都是私有的。 10.1.6?default子句 default子句用來允許用戶控制并行區域中變量的共享屬性。 用法如下: default(shared|none) 使用shared時,缺省情況下,傳入并行區域內的同名變量被當作共享變量來處理,不會產生線程私有副本,除非使用private等子句來指定某些變量為私有的才會產生副本。 如果使用none作為參數,那么線程中用到的變量必須顯示指定是共享的還是私有的,除了那些由明確定義的除外。 10.1.7?reduction子句 reduction子句主要用來對一個或多個參數條目指定一個操作符,每個線程將創建參數條目的一個私有拷貝,在區域的結束處,將用私有拷貝的值通過指定的運行符運算,原始的參數條目被運算結果的值更新。 reduction子句用法如下: reduction(operator:list) 下表列出了可以用于reduction子句的一些操作符以及對應私有拷貝變量缺省的初始值,私有拷貝變量的實際初始值依賴于redtucion變量的數據類型。 表10-4-1:reduction操作中各種操作符號對應拷貝變量的缺省初始值| Operator | Initialization value |
| + | 0 |
| * | 1 |
| - | 0 |
| & | ~0 |
| | | 0 |
| ^ | 0 |
| && | 1 |
| || | 0 |
Ananth?Grama, Anshul Gupta,“并行計算導論”,張武等譯,機械工業出版社,2005.01
Michael J. Quinn, “MPI與OpenMP并行程序設計”,陳文光等譯,清華大學出版社,2004.10
Shameem Akhter等,“多核程序設計技術-通過軟件多線程提升性能”,電子工業出版社,2007.03 OpenMP2.5規范http://www.openmp.org/ OpenMP2.0規范http://www.openmp.org/ MSDN幫助材料ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.VisualStudio.v80.chs/dv_vclang/html/652414c5-78ed-4b7f-8283-1a9fe4c5e78d.htm?? 注:本文的寫作主要參考OpenMP2.5規范等參考文獻,限于水平,可能存在對規范理解不足之處,請大家發現問題時不吝指正。4、OpenMP中的任務調度????????????? (OpenMP線程同步)
OpenMP中的任務調度 OpenMP中,任務調度主要用于并行的for循環中,當循環中每次迭代的計算量不相等時,如果簡單地給各個線程分配相同次數的迭代的話,會造成各個線程計算負載不均衡,這會使得有些線程先執行完,有些后執行完,造成某些CPU核空閑,影響程序性能。例如以下代碼: int i, j; int a[100][100] = {0}; for ( i =0; i < 100; i++) { for( j = i; j < 100; j++ ) { a[i][j] = i*j; } } 如果將最外層循環并行化的話,比如使用4個線程,如果給每個線程平均分配25次循環迭代計算的話,顯然i=0和i=99的計算量相差了100倍,那么各個線程間可能出現較大的負載不平衡情況。為了解決這些問題,OpenMP中提供了幾種對for循環并行化的任務調度方案。 在OpenMP中,對for循環并行化的任務調度使用schedule子句來實現,下面介紹schedule字句的用法。 1.1.1??????Schedule子句用法 schedule子句的使用格式為: schedule(type[,size]) schedule有兩個參數:type和size,size參數是可選的。 1.??????type參數 表示調度類型,有四種調度類型如下: ·?dynamic ·?guided ·?runtime ·?static 這四種調度類型實際上只有static、dynamic、guided三種調度方式,runtime實際上是根據環境變量來選擇前三種中的某中類型。 run-sched-var 2.??????size參數 (可選) size參數表示循環迭代次數,size參數必須是整數。static、dynamic、guided三種調度方式都可以使用size參數,也可以不使用size參數。當type參數類型為runtime時,size參數是非法的(不需要使用,如果使用的話編譯器會報錯)。 1.1.2??????靜態調度(static) 當parallel for編譯指導語句沒有帶schedule子句時,大部分系統中默認采用static調度方式,這種調度方式非常簡單。假設有n次循環迭代,t個線程,那么給每個線程靜態分配大約n/t次迭代計算。這里為什么說大約分配n/t次呢?因為n/t不一定是整數,因此實際分配的迭代次數可能存在差1的情況,如果指定了size參數的話,那么可能相差一個size。 靜態調度時可以不使用size參數,也可以使用size參數。 3.??????不使用size參數 不使用size參數時,分配給每個線程的是n/t次連續的迭代,不使用size參數的用法如下: schedule(static) 例如以下代碼: ??? #pragma omp parallelfor schedule(static) ??? for(i = 0; i < 10; i++ ) ??? { ???????? printf("i=%d, thread_id=%d/n", i, omp_get_thread_num()); } 上面代碼執行時打印的結果如下: i=0, thread_id=0 i=1, thread_id=0 i=2, thread_id=0 i=3, thread_id=0 i=4, thread_id=0 i=5, thread_id=1 i=6, thread_id=1 i=7, thread_id=1 i=8, thread_id=1 i=9, thread_id=1 可以看出線程0得到了0~4次連續迭代,線程1得到5~9次連續迭代。注意由于多線程執行時序的隨機性,每次執行時打印的結果順序可能存在差別,后面的例子也一樣。 4.??????使用size參數 使用size參數時,分配給每個線程的size次連續的迭代計算,用法如下: schedule(static, size) 例如以下代碼: ??? #pragma omp parallelfor schedule(static, 2) ??? for(i = 0; i < 10; i++ ) ??? { ???????? printf("i=%d, thread_id=%d/n", i, omp_get_thread_num()); } 執行時會打印以下結果: i=0, thread_id=0 i=1, thread_id=0 i=4, thread_id=0 i=5, thread_id=0 i=8, thread_id=0 i=9, thread_id=0 i=2, thread_id=1 i=3, thread_id=1 i=6, thread_id=1 i=7, thread_id=1 從打印結果可以看出,0、1次迭代分配給線程0,2、3次迭代分配給線程1,4、5次迭代分配給線程0,6、7次迭代分配給線程1,…。每個線程依次分配到2次連續的迭代計算。 1.1.3??????動態調度(dynamic) 動態調度是動態地將迭代分配到各個線程,動態調度可以使用size參數也可以不使用size參數,不使用size參數時是將迭代逐個地分配到各個線程,使用size參數時,每次分配給線程的迭代次數為指定的size次。 下面為使用動態調度不帶size參數的例子: #pragma omp parallel for schedule(dynamic) for(i = 0; i < 10; i++ ) { printf("i=%d, thread_id=%d/n", i, omp_get_thread_num()); } 打印結果如下: i=0, thread_id=0 i=1, thread_id=1 i=2, thread_id=0 i=3, thread_id=1 i=5, thread_id=1 i=6, thread_id=1 i=7, thread_id=1 i=8, thread_id=1 i=4, thread_id=0 i=9, thread_id=1 下面為動態調度使用size參數的例子: #pragma omp parallel for schedule(dynamic, 2) for(i = 0; i < 10; i++ ) { printf("i=%d, thread_id=%d/n", i, omp_get_thread_num()); } 打印結果如下: i=0, thread_id=0 i=1, thread_id=0 i=4, thread_id=0 i=2, thread_id=1 i=5, thread_id=0 i=3, thread_id=1 i=6, thread_id=0 i=8, thread_id=1 i=7, thread_id=0 i=9, thread_id=1 從打印結果可以看出第0、1,4、5,6、7次迭代被分配給了線程0,第2、3,8、9次迭代則分配給了線程1,每次分配的迭代次數為2。 1.1.4??????guided調度(guided) guided調度是一種采用指導性的啟發式自調度方法。開始時每個線程會分配到較大的迭代塊,之后分配到的迭代塊會逐漸遞減。迭代塊的大小會按指數級下降到指定的size大小,如果沒有指定size參數,那么迭代塊大小最小會降到1。 例如以下代碼: ??? #pragma omp parallelfor schedule(guided,2) ??? for(i = 0; i < 10; i++ ) ??? { ???????? printf("i=%d, thread_id=%d/n", i, omp_get_thread_num()); } 打印結果如下: i=0, thread_id=0 i=1, thread_id=0 i=2, thread_id=0 i=3, thread_id=0 i=4, thread_id=0 i=8, thread_id=0 i=9, thread_id=0 i=5, thread_id=1 i=6, thread_id=1 i=7, thread_id=1 第0、1、2、3、4次迭代被分配給線程0,第5、6、7次迭代被分配給線程1,第8、9次迭代被分配給線程0,分配的迭代次數呈遞減趨勢,最后一次遞減到2次。 1.1.5??????runtime調度(rumtime) runtime調度并不是和前面三種調度方式似的真實調度方式,它是在運行時根據環境變量OMP_SCHEDULE來確定調度類型,最終使用的調度類型仍然是上述三種調度方式中的某種。 例如在unix系統中,可以使用setenv命令來設置OMP_SCHEDULE環境變量: setenv OMP_SCHEDULE?“dynamic, 2” 上述命令設置調度類型為動態調度,動態調度的迭代次數為2。 在windows環境中,可以在”系統屬性|高級|環境變量”對話框中進行設置環境變量。5、OpenMP創建線程中的鎖及原子操作性能比較
OpenMP創建線程中的鎖及原子操作性能比較 相關文檔連接: 多核編程中的任務隨機競爭模式的概率分析 多核編程中的任務分組競爭模式???????????? ?多核編程中的負載平衡難題 多核編程中的鎖競爭難題 多核編程的幾個難題及其應對策略(難題一) OpenMP并行程序設計(二) ?OpenMP并行程序設計(一) ?雙核CPU上的快速排序效率???????????? 在多核CPU中鎖競爭到底會造成性能怎樣的下降呢?相信這是許多人想了解的,因此特地寫了一個測試程序來測試原子操作,windows CriticalSection, OpenMP的鎖操作函數在多核CPU中的性能。 原子操作選用InterlockedIncrement來進行測試, 對每種鎖和原子操作,都測試在單任務執行和多任務執行2000000次加鎖解鎖操作所消耗的時間。 測試的詳細代碼見后面。 測試機器環境:Intel 2.66G 雙核CPU 機器一臺 測試運行結果如下: SingleThread, InterlockedIncrement 2,000,000: a = 2000000, time = 78 MultiThread, InterlockedIncrement 2,000,000: a = 2000000, time = 156 SingleThread, Critical_Section 2,000,000:a = 2000000, time = 172 MultiThread, Critical_Section, 2,000,000:a = 2000000, time = 3156 SingleThread,omp_lock 2,000,000:a = 2000000, time = 250 MultiThread,omp_lock 2,000,000:a = 2000000, time = 1063 在單任務運行情況下,所消耗的時間如下: 原子操作?? ??????????????78ms Windows CriticalSection?172ms OpenMP的lock操作??????? 250ms 因此從單任務情況來看,原子操作最快,Windows CriticalSection次之,OpenMP庫帶的鎖最慢,但這幾種操作的時間差距不是很大,用鎖操作比原子操作慢了2~3倍左右。 在多個任務運行的情況下,所消耗的時間如下: 原子操作?? ??????????????156ms Windows CriticalSection?3156ms OpenMP的lock操作??????? 1063ms 在多任務運行情況下,情況發生了意想不到的變化,原子操作時間比單任務操作時慢了一倍,在兩個CPU上運行比在單個CPU上運行還慢一倍,真是難以想象,估計是任務切換開銷造成的。 Windows CriticalSection則更離譜了,居然花了3156ms,是單任務運行時的18倍多的時間,慢得簡直無法想象。 OpenMP的lock操作比Windows CriticalSection稍微好一些,但也花了1063ms,是單任務時的7倍左右。 由此可以知道,在多核CPU的多任務環境中,原子操作是最快的,而OpenMP次之,Windows CriticalSection則最慢。 同時從這些鎖在單任務和多任務下的性能差距可以看出,,多核CPU上的編程和以往的單核多任務編程會有很大的區別。 需要說明的是,本測試是一種極端情況下的測試,鎖住的操作只是一個簡單的加1操作,并且鎖競爭次數達200萬次之多,在實際情況中,一由于任務中還有很多不需要加鎖的代碼在運行,實際情況中的性能會比本測試的性能好很多。 測試代碼如下: // TestLock.cpp : OpenMP任務中的原子操作和鎖性能測試程序。 // #include<windows.h> #include<time.h> #include<process.h> #include<omp.h> #include<stdio.h> void TestAtomic() { ???? clock_t?t1,t2; ???? int????? i = 0; ???? volatile LONG????? a = 0; ???? t1 = clock(); ???? for( i = 0; i < 2000000; i++ ) ???? { ???????? InterlockedIncrement( &a); ???? } ???? ???? t2 = clock(); ???? printf("SingleThread, InterlockedIncrement 2,000,000: a = %ld, time = %ld/n", a, t2-t1); ???? t1 = clock(); #pragma omp parallelfor ???? for( i = 0; i < 2000000; i++ ) ???? { ???????? InterlockedIncrement( &a); ???? } ???? ???? t2 = clock(); ???? printf("MultiThread, InterlockedIncrement 2,000,000: a = %ld, time = %ld/n", a, t2-t1); } void TestOmpLock() { ???? clock_t t1,t2; ???? int i; ???? int a = 0; ???? omp_lock_t??? mylock; ???? omp_init_lock(&mylock); ???? t1 = clock(); ???? for( i = 0; i < 2000000; i++ ) ???? { ???????? omp_set_lock(&mylock); ???????? a+=1; ???????? omp_unset_lock(&mylock); ???? } ???? t2 = clock(); ???? ???? printf("SingleThread,omp_lock 2,000,000:a = %ld, time = %ld/n", a, t2-t1); ???? t1 = clock(); #pragma omp parallelfor ???? for( i = 0; i < 2000000; i++ ) ???? { ???????? omp_set_lock(&mylock); ???????? a+=1; ???????? omp_unset_lock(&mylock); ???? } ???? t2 = clock(); ???? ???? printf("MultiThread,omp_lock 2,000,000:a = %ld, time = %ld/n", a, t2-t1); ???? omp_destroy_lock(&mylock); } void TestCriticalSection() { ???? clock_t t1,t2; ???? int i; ???? int?a = 0; ???? CRITICAL_SECTION?? cs; ???? InitializeCriticalSection(&cs); ???? t1 = clock(); ???? for( i = 0; i < 2000000; i++ ) ???? { ???????? EnterCriticalSection(&cs); ???????? a+=1; ???????? LeaveCriticalSection(&cs); ???? } ???? t2 = clock(); ???? printf("SingleThread, Critical_Section 2,000,000:a = %ld, time = %ld/n", a, t2-t1); ???? t1 = clock(); #pragma omp parallelfor ???? for( i = 0; i < 2000000; i++ ) ???? { ???????? EnterCriticalSection(&cs); ???????? a+=1; ???????? LeaveCriticalSection(&cs); ???? } ???? t2 = clock(); ???? printf("MultiThread, Critical_Section, 2,000,000:a = %ld, time = %ld/n", a, t2-t1); ???? DeleteCriticalSection(&cs); } int main(int argc,char* argv[]) { ???? TestAtomic(); ???? TestCriticalSection(); ???? TestOmpLock(); ???? return 0; }6、OpenMP程序設計的兩個小技巧
???????OpenMP程序設計的兩個小技巧
1、動態設置并行循環的線程數量 在實際情況中,程序可能運行在不同的機器環境里,有些機器是雙核,有些機器是4核甚至更多核。并且未來硬件存在升級的可能,CPU核數會變得越來越多。如何根據機器硬件的不同來自動設置合適的線程數量就顯得很重要了,否則硬件升級后程序就得進行修改,那將是一件很麻煩的事情。 比如剛開始在雙核系統中開發的軟件,線程數量缺省都設成2,那么當機器升級到4核或8核以后,線程數量就不能滿足要求了,除非修改程序。 線程數量的設置除了要滿足機器硬件升級的可擴展性外,還需要考慮程序的可擴展性,當程序運算量增加或減少后,設置的線程數量仍然能夠滿足要求。顯然這也不能通過設置靜態的線程數量來解決。 在具體計算需要使用多少線程時,主要需要考慮以下兩點: 1)?????????????當循環次數比較少時,如果分成過多數量的線程來執行,可能會使得總運行時間高于較少線程或一個線程執行的情況。并且會增加能耗。 2)?????????????如果設置的線程數量遠大于CPU核數的話,那么存在著大量的任務切換和調度等開銷,也會降低整體效率。 那么如何根據循環的次數和CPU核數來動態地設置線程的數量呢?下面以一個例子來說明動態設置線程數量的算法,假設一個需要動態設置線程數的需求為: 1、?以多個線程運行時的每個線程運行的循環次數不低于4次 2、?總的運行線程數最大不超過2倍CPU核數 下面代碼便是一個實現上述需求的動態設置線程數量的例子 ?? constint MIN_ITERATOR_NUM = 4; ?? int ncore = omp_get_num_procs();//獲取執行核的數量 ?? int max_tn = n / MIN_ITERATOR_NUM; ?? int tn = max_tn > 2*ncore ? 2*ncore : max_tn;?//tn表示要設置的線程數量 #pragma omp parallelforif( tn > 1) num_threads(tn) ???? for ( i = 0; i < n; i++ ) ???? { ???????? printf("Thread Id = %ld/n", omp_get_thread_num()); ???????? //Do some work here ???? } 在上面代碼中,根據每個線程運行的循環次數不低于4次,先計算出最大可能的線程數max_tn,然后計算需要的線程數量tn,tn的值等于max_tn和2倍CPU核數中的較小值。 然后在parallel for構造中使用if子句來判斷tn是否大于1,大于1時使用單個線程,否則使用tn個線程,,這樣就使得設置的線程數量滿足了需求中的條件。 比如在一個雙核CPU上,n=64,最終會以2倍CPU核數(4個)線程運行,而不會以max_tn = 64/4=16個線程運行。 在實際情況中,當然不能每個循環都象上面一樣寫幾行代碼來計算一遍,可以將其寫成一個獨立的功能函數如下: const int g_ncore = omp_get_num_procs(); //獲取執行核的數量 /**?計算循環迭代需要的線程數量 ???? 根據循環迭代次數和CPU核數及一個線程最少需要的循環迭代次數 ???? 來計算出需要的線程數量,計算出的最大線程數量不超過CPU核數 ???? @param?? int n - 循環迭代次數?? ???? @param?? int min_n - 單個線程需要的最少迭代次數??? ???? @return?int - 線程數量???? */ int dtn(int n, int min_n) { ?? int max_tn = n / min_n; ?? int tn = max_tn > g_ncore ? g_ncore : max_tn;?//tn表示要設置的線程數量 ?? if ( tn < 1 ) ?? { ???? ?? tn = 1; ?? } ?? return tn; } 這樣每次并行化循環時就可以直接使用函數dtn()來獲取合適的線程數量,前面的代碼可以簡寫成如下形式: #pragma omp parallelfor num_threads(dtn(n, MIN_ITERATOR_NUM)) ???? for ( i = 0; i < n; i++ ) ???? { ???????? printf("Thread Id = %ld/n", omp_get_thread_num()); ???????? //Do some work here ???? } 當然具體設置多少線程要視情況而定的,一般情況下線程數量剛好等于CPU核數可以取得比較好的性能,因為線程數等于CPU核數時,每個核執行一個任務,沒有任務切換開銷。 2、嵌套循環的并行化 在嵌套循環中,如果外層循環迭代次數較少時,如果將來CPU核數增加到一定程度時,創建的線程數將可能小于CPU核數。另外如果內層循環存在負載平衡的情況下,很難調度外層循環使之達到負載平衡。 下面以矩陣乘法作為例子來講述如何將嵌套循環并行化,以滿足上述擴展性和負載平衡需求。 一個串行的矩陣乘法的函數代碼如下: /**?矩陣串行乘法函數 ??? @param??? int *a - 指向要相乘的第個矩陣的指針 ??? @param??? int row_a - 矩陣a的行數 ??? @param??? int col_a - 矩陣a的列數 ??? @param??? int *b - 指向要相乘的第個矩陣的指針? ??? @param??? int row_b - 矩陣b的行數 ??? @param??? int col_b - 矩陣b的列數 ??? @param??? int *c - 計算結果的矩陣的指針 ??? @param??? int c_size - 矩陣c的空間大小(總元素個數) ??? @return?? void - 無 */ void Matrix_Multiply(int *a, int row_a, int col_a, ????????????????????? int *b, int row_b,int col_b, ????????????????????? int *c, int c_size) { ??? if ( col_a != row_b || c_size < row_a * col_b ) ??? { ??????? return; ??? } ??? int i, j, k; //#pragma omp for private(i, j, k) ??? for ( i = 0; i < row_a; i++ ) ??? { ??????? int row_i = i * col_a; ??????? int row_c = i * col_b; ??????? for ( j = 0; j < col_b; j++ ) ??????? { ???????????c[row_c + j] = 0; ??????????? for ( k = 0; k < row_b; k++ ) ??????????? { ??????????????? c[row_c + j] += a[row_i + k] * b[k * col_b + j]; ??????????? } ??????? } ??? } } 如果在外層循環前加上OpenMP的for語句時,它就變成了一個并行的矩陣乘法函數,但是這樣簡單地將其并行化顯然無法滿足前面所述的擴展性需求。 其實可以采用一個簡單的方法將最外層循環和第2層循環合并成一個循環,下面便是采用合并循環后的并行實現。 void Parallel_Matrix_Multiply(int *a, int row_a, int col_a, ???????????????????? int *b, int row_b,int col_b, ???????????????????? int *c, int c_size ) { ??? if ( col_a != row_b ) ??? { ??????? return; ??? } ??? int i, j, k; ??? int index; ??? int border = row_a * col_b; ??? i = 0; ??? j = 0; #pragma omp parallel private(i,j,k) num_threads(dtn(border, 1)) ??? for ( index = 0; index < border; index++ ) ??? { ??????? i = index / col_b; ??????? j = index % col_b; ??????? int row_i = i * col_a; ??????? int row_c = i * col_b; ??????? c[row_c+j] = 0; ??????? for ( k = 0; k < row_b; k++ ) ??????? { ??????????? c[row_c + j] += a[row_i+k] * b[k*col_b+j]; ??????? } ??? } } 從上面代碼可以看出,合并后的循環邊界border = row_a * col_b;即等于原來兩個循環邊界之積,然后在循環中計算出原來的外層循環和第2層循環的迭代變量i和j,采用除法和取余來求出i和j的值。 需要注意的是,上面求i和j的值必須要保證循環迭代的獨立性,即不能有循環迭代間的依賴關系。不能將求i和j值的過程優化成如下的形式: if ( j == col_b ) { ???? j = 0; ???? i++; } // …… 此處代表實際的矩陣乘法代碼 j++; 上面這種優化,省去了除法,效率高,但是只能在串行代碼中使用,因為它存在循環迭代間的依賴關系,無法將其正確地并行化。
??????? 上面列出的這些OpenMP知識,屬于初步的入門知識,如果需要進一步深入掌握OpenMP或者了解其實現原理,則需要看更多的參考文獻。下面列出我寫的《多核計算與程序設計》一書的第3章OpenMP程序設計中的參考文獻,供需要深入掌握的人參考。其中的文獻【2】講解了OpenMP的實現原理。
【1】 Ananth Grama, Anshul Gupta,“并行計算導論”,張武等譯,機械工業出版社,2005.01
【2】 Barbara Chapman, “How OpenMP is Compiled ”,http://cobweb.ecn.purdue.edu/ParaMount/iwomp2008/documents/chapman-underthehood
【3】 Bruce McMillin等,“Parallel Algorithm Fundamentals and Analysis”,http://citeseer.ist.psu.edu/mcmillin93parallel.html
【4】 Common Language Infrastructure (CLI) Partitions I to VIhttp://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf
【5】 Introduction to OpenMP,A Directive-based API for Parallel Processing on Shared-memory Computers,http://scv.bu.edu/documentation/tutorials/OpenMP/
【6】 Michael J. Quinn, “MPI與OpenMP并行程序設計”,陳文光等譯,清華大學出版社,2004.10
【7】 Mitsuhisa Sato, Shigehisa Satoh, Kazuhiro Kusano and Yoshio Tanaka, “Design of OpenMP Compiler for an SMP Cluster”,
http://www.hpcs.is.tsukuba.ac.jp/~msato/pdplab/papers/ewomp99.pdf
【8】 MSDN幫助材料
ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.VisualStudio.v80.chs/dv_vclang/html/652414c5-78ed-4b7f-8283-1a9fe4c5e78d.htm
【9】 Omni OpenMP compiler, http://phase.hpcc.jp/omni/home.html.
【10】 OpenMP2.0規范http://www.openmp.org/
【11】 OpenMP2.5規范http://www.openmp.org/
【12】 OpenMP: Simple, portable, scalable SMP Programming, http://www.OpenMP.org.
【13】 Rudolf Eigenmann and Timothy G. mattson. “OpenMP tutorial, part 2: Advanced OpenMP.”,http://www.cise.ufl.edu/research/ParallelPatterns/sc01-omp-tut-advanced.ppt.
【14】 Ruud van der Pas ,“An Introduction Into OpenMP”,http://www.nic.uoregon.edu/iwomp2005/iwomp2005_tutorial_openmp_rvdp.pdf
【15】 Sanjiv Shah, Grant Haab, Paul Petersen, & Joe Throop,“Flexible Control Structures for Parallelism in OpenMP”,http://www.it.lth.se/ewomp99/papers/grant.pdf
【16】 Shameem Akhter等,“多核程序設計技術-通過軟件多線程提升性能”,電子工業出版社,2007.03
【17】 Special issue on OpenMP and its applications. Scientific Programming, 11(2),2003.
【18】 Y. Charlie Hu, Honghui Lu, Alan L. Cox, and Willy Zwaenepoel.“OpenMP for networks of SMPs”,In Proceedings of 13th International Parallel Processing Symposium and 10th Symposium on Parallel and Distributed Processing, page 302-310. IEEE Computer Society, 1999.
多核編程相關文章:
1)用原子操作解決多線程退出問題
2)原子操作在多核編程中的使用
3)多核編程偽共享問題及其對策
4)多核編程鎖競爭問題及其對策
5)多核分布式隊列的實現:“偷”與“自私”的運用
6)多核編程中的條件同步模式
7)多核編程的四層境界
8)“老子”是偉大的多核計算科學家
9)程序員的十層樓(1~3層)
10)多核編程文章匯總
11)并行順序搜索及終止檢測
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的OpenMP: OpenMP编程指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OpenMP 编程实例(蒙特卡罗算法)
- 下一篇: 多类SVM的损失函数