OpenMP共享內存並行編程詳解

 

實驗平臺:win7, VS2010html

 

1. 介紹ios

    並行計算機能夠簡單分爲共享內存和分佈式內存,共享內存就是多個核心共享一個內存,目前的PC就是這類(不論是隻有一個多核CPU仍是能夠插多個CPU,它們都有多個核心和一個內存),通常的大型計算機結合分佈式內存和共享內存結構,即每一個計算節點內是共享內存,節點間是分佈式內存。想要在這些並行計算機上得到較好的性能,進行並行編程是必要條件。目前流行的並行程序設計方法是,分佈式內存結構上使用MPI,共享內存結構上使用Pthreads或OpenMP。咱們這裏關注的是共享內存並行計算機,由於編輯這篇文章的機器就屬於此類型(普通的臺式機)。和Pthreads相比OpenMP更簡單,對於關注算法、只要求對線程之間關係進行最基本控制(同步,互斥等)的咱們來講,OpenMP再適合不過了。程序員

    本文對windows上Visual Studio開發環境下的OpenMP並行編程進行簡單的探討。本文參考了wikipedia關於OpenMP條目、OpenMP.org(有OpenMP Specification)、MSDM上關於OpenMP條目以及教材《MPI與OpenMP並行程序設計(C語言版)》:算法

  1. http://zh.wikipedia.org/wiki/OpenMP
  2. http://openmp.org/
  3. http://msdn.microsoft.com/en-us/library/tt15eb9t(v=vs.100).aspx
  4. 《MPI與OpenMP並行程序設計(C語言版)》第17章,Michael J. Quinn著,陳文光等譯,清華大學出版社,2004

    注意,OpenMP目前最新版本爲4.0.0,而VS2010僅支持OpenMP2.0(2002年版本),因此本文所講的也是OpenMP2.0,本文注重使用OpenMP得到接近核心數的加速比,因此OpenMP2.0也足夠了。express

 

2. 第一個OpenMP程序編程

step 1: 新建控制檯程序windows

step 2: 項目屬性,全部配置下「配置屬性>>C/C++>>語言>>OpenMP支持」修改成是(/openmp),以下圖:多線程

step 3: 添加以下代碼:分佈式

 1 #include<omp.h>
 2 #include<iostream>
 3 int main()
 4 {
 5     std::cout << "parallel begin:\n";
 6     #pragma omp parallel
 7     {
 8         std::cout << omp_get_thread_num();
 9     }
10     std::cout << "\n parallel end.\n";
11     std::cin.get();
12     return 0;
13 }

step 4: 運行結果以下圖:ide

能夠看到,個人計算機是8核的(嚴格說是8線程的),這是咱們實驗室的小型工做站(至多支持24核)。

 

3. 「第一個OpenMP程序」幕後,並行原理

    OpenMP由Compiler Directives(編譯指導語句)、Run-time Library Functions(庫函數)組成,另外還有一些和OpenMP有關的Environment Variables(環境變量)、Data Types(數據類型)以及_OPENMP宏定義。之因此說OpenMP很是簡單,是由於,全部這些總共只有50個左右,OpenMP2.0 Specification僅有100餘頁。第2節的「第一個OpenMP程序」的第6行「#pragma omp parallel」即Compiler Directive,「#pragma omp parallel」下面的語句將被多個線程並行執行(也即被執行不止一遍),第8行的omp_get_thread_num()即Run-time Library Function,omp_get_thread_num()返回當前執行代碼所在線程編號。

    共享內存計算機上並行程序的基本思路就是使用多線程,從而將可並行負載分配到多個物理計算核心,從而縮短執行時間(同時提升CPU利用率)。在共享內存的並行程序中,標準的並行模式爲fork/join式並行,這個基本模型以下圖示:

其中,主線程執行算法的順序部分,當遇到須要進行並行計算式,主線程派生出(建立或者喚醒)一些附加線程。在並行區域內,主線程和這些派生線程協同工做,在並行代碼結束時,派生的線程退出或者掛起,同時控制流回到單獨的主線程中,稱爲匯合。對應第2節的「第一個OpenMP程序」,第4行對應程序開始,4-5行對應串行部分,6-9行對應第一個並行塊(8個線程),10-13行對應串行部分,13行對應程序結束。

    簡單來講,OpenMP程序就是在通常程序代碼中加入Compiler Directives,這些Compiler Directives指示編譯器其後的代碼應該如何處理(是多線程執行仍是同步什麼的)。因此說OpenMP須要編譯器的支持。上一小節的step 2即打開編譯器的OpenMP支持。和Pthreads不一樣,OpenMP下程序員只須要設計高層並行結構,建立及調度線程均由編譯器自動生成代碼完成。

 

4. Compiler Directives

4.1 通常格式

Compiler Directive的基本格式以下:

#pragma omp directive-name [clause[ [,] clause]...]

其中「[]」表示可選,每一個Compiler Directive做用於其後的語句(C++中「{}」括起來部分是一個複合語句)。

directive-name能夠爲:parallel, for, sections, single, atomic, barrier, critical, flush, master, ordered, threadprivate(共11個,只有前4個有可選的clause)。

clause(子句)至關因而Directive的修飾,定義一些Directive的參數什麼的。clause能夠爲:copyin(variable-list), copyprivate(variable-list), default(shared | none), firstprivate(variable-list), if(expression), lastprivate(variable-list), nowait, num_threads(num), ordered, private(variable-list), reduction(operation: variable-list), schedule(type[,size]), shared(variable-list)(共13個)。

    例如「#pragma omp parallel」表示其後語句將被多個線程並行執行,線程個數由系統預設(通常等於邏輯處理器個數,例如i5 4核8線程CPU有8個邏輯處理器),能夠在該directive中加入可選的clauses,如「#pragma omp parallel num_threads(4)」仍舊錶示其後語句將被多個線程並行執行,可是線程個數爲4。

4.2 詳細解釋

    本節的敘述順序同個人另外一篇博文:OpenMP編程總結表,讀者能夠對照閱讀,也能夠快速預覽OpenMP全部語法。

    若是沒有特殊說明,程序均在Debug下編譯運行。

parallel                                        

    parallel表示其後語句將被多個線程並行執行,這已經知道了。「#pragma omp parallel」後面的語句(或者,語句塊)被稱爲parallel region。

    能夠用if clause條件地進行並行化,用num_threads clause覆蓋默認線程數:

1 int a = 0;
2 #pragma omp parallel if(a) num_threads(6)
3 {
4     std::cout << omp_get_thread_num();
5 }

int a = 7;
#pragma omp parallel if(a) num_threads(6)
{
    std::cout << omp_get_thread_num();
}

能夠看到多個線程的執行順序是不能保證的。

    private, firstprivate, shared, default, reduction, copyin clauses留到threadprivate directive時說。

for                                        

    第2節的「第一個OpenMP程序」其實不符合咱們對並行程序的預期——咱們通常並非要對相同代碼在多個線程並行執行,而是,對一個計算量龐大的任務,對其進行劃分,讓多個線程分別執行計算任務的每一部分,從而達到縮短計算時間的目的。這裏的關鍵是,每一個線程執行的計算互不相同(操做的數據不一樣或者計算任務自己不一樣),多個線程協做完成全部計算。OpenMP for指示將C++ for循環的屢次迭代劃分給多個線程(劃分指,每一個線程執行的迭代互不重複,全部線程的迭代並起來正好是C++ for循環的全部迭代),這裏C++ for循環須要一些限制從而能在執行C++ for以前肯定循環次數,例如C++ for中不該含有break等。OpenMP for做用於其後的第一層C++ for循環。下面是一個例子:

1 const int size = 1000;
2 int data[size];
3 #pragma omp parallel
4 {
5     #pragma omp for
6     for(int i=0; i<size; ++i)
7         data[i] = 123;
8 }

默認狀況下,上面的代碼中,程序執行到「#pragma omp parallel」處會派生出7和線程,加上主線程共8個線程(在個人機器上),C++ for的1000次迭代會被分紅連續的8段——0-124次迭代由0號線程計算,125-249次迭代由1號線程計算,以此類推。可能你已經猜到了,具體C++ for的各次迭代在線程間如何分配能夠由clause指示,它就是schedule(type[,size]),後面會具體說。

    若是parallel region中只包含一個for directive做用的語句,上面代碼就是這種狀況,此時能夠將parallel和for「縮寫」爲parallel for,上面代碼等價於這樣:

1 const int size = 1000;
2 int data[size];
3 #pragma omp parallel for
4 for(int i=0; i<size; ++i)
5     data[i] = 123;

    正確使用for directive有兩個條件,第1是C++ for符合特定限制,不然編譯器將報告錯誤,第2是C++ for的各次迭代的執行順序不影響結果正確性,這是一個邏輯條件。例子以下:

1 #pragma omp parallel num_threads(6)
2 {
3     #pragma omp for
4     for(int i=0; i<1000000; ++i)
5         if(i>999)
6             break;
7 }

編譯器報錯以下:

error C3010: 「break」: 不容許跳出 OpenMP 結構化塊

    schedule(type[,size])設置C++ for的屢次迭代如何在多個線程間劃分:

  1. schedule(static, size)將全部迭代按每連續size個爲一組,而後將這些組輪轉分給各個線程。例若有4個線程,100次迭代,schedule(static, 5)將迭代:0-4, 5-9, 10-14, 15-19, 20-24...依次分給0, 1, 2, 3, 0...號線程。schedule(static)同schedule(static, size_av),其中size_av等於迭代次數除以線程數,即將迭代分紅連續的和線程數相同的等分(或近似等分)。
  2. schedule(dynamic, size)一樣分組,而後依次將每組分給目前空閒的線程(故叫動態)。
  3. schedule(guided, size) 把迭代分組,分配給目前空閒的線程,最初組大小爲迭代數除以線程數,而後逐漸按指數方式(依次除以2)降低到size。
  4. schedule(runtime)的劃分方式由環境變量OMP_SCHEDULE定義。

下面是幾個例子,能夠先忽略critical directive:

1 #pragma omp parallel num_threads(3)
2 {
3     #pragma omp for
4     for(int i=0; i<9; ++i){
5         #pragma omp critical
6         std::cout << omp_get_thread_num() << i << " ";
7     }
8 }

上面輸出說明0號線程執行0-2迭代,1號執行3-5,2號執行6-9,至關於schedule(static, 3)。

1 #pragma omp parallel num_threads(3)
2 {
3     #pragma omp for schedule(static, 1)
4     for(int i=0; i<9; ++i){
5         #pragma omp critical 
6         std::cout << omp_get_thread_num() << i << " ";
7     }
8 }

1 #pragma omp parallel num_threads(3)
2 {
3     #pragma omp for schedule(dynamic, 2)
4     for(int i=0; i<9; ++i){
5         #pragma omp critical 
6         std::cout << omp_get_thread_num() << i << " ";
7     }
8 }

    ordered clause配合ordered directive使用,請見ordered directive,nowait留到barrier directive時說,private, firstprivate, lastprivate, reduction留到threadprivate directive時說。

sections                                        

    若是說for directive用做數據並行,那麼sections directive用於任務並行,它指示後面的代碼塊包含將被多個線程並行執行的section塊。下面是一個例子:

 1 #pragma omp parallel
 2 {
 3     #pragma omp sections
 4     {
 5         #pragma omp section
 6         std::cout << omp_get_thread_num();
 7         #pragma omp section
 8         std::cout << omp_get_thread_num();
 9     }
10 }

上面代碼中2個section塊將被2個線程並行執行,多個個section塊的第1個「#pragma omp section」能夠省略。這裏有些問題,執行這段代碼是總共會有多少個線程呢,「#pragma omp parallel」沒有clause,默認是8個線程(又說的在個人機器上),2個section是被哪2個線程執行是不肯定的,當section塊多於8個時,會有一個線程執行不止1個section塊。

    一樣,上面代碼能夠「縮寫」爲parallel sections

1 #pragma omp parallel sections
2 {
3     #pragma omp section
4     std::cout << omp_get_thread_num();
5     #pragma omp section
6     std::cout << omp_get_thread_num();
7 }

    nowait clause留到barrier directive時說,private, firstprivate, lastprivate, reduction clauses留到threadprivate directive時說。

single                                        

    指示代碼將僅被一個線程執行,具體是哪一個線程不肯定,例子以下:

1 #pragma omp parallel num_threads(4)
2 {
3     #pragma omp single
4     std::cout << omp_get_thread_num();
5     std::cout << "-";
6 }

這裏0號線程執行了第4 5兩行代碼,其他三個線程執行了第5行代碼。

    nowait clause留到barrier directive時說,private, firstprivate, copyprivate clauses留到threadprivate directive時說。

master                                        

    指示代碼將僅被主線程執行,功能相似於single directive,但single directive時具體是哪一個線程不肯定(有多是當時閒的那個)。

critical                                        

    定義一個臨界區,保證同一時刻只有一個線程訪問臨界區。觀察以下代碼及其結果:

1 #pragma omp parallel num_threads(6)
2 {
3     std::cout << omp_get_thread_num() << omp_get_thread_num();
4 }

5號線程執行第3行代碼時被2號線程打斷了(並非每次運行均可能出現打斷)。

1 #pragma omp parallel num_threads(6)
2 {
3     #pragma omp critical
4     std::cout << omp_get_thread_num() << omp_get_thread_num();
5 }

此次無論運行多少遍都不會出現某個數字不是連續兩個出現,由於在第4行代碼被一個線程執行期間,其餘線程不能執行(該行代碼是臨界區)。

barrier                                        

    定義一個同步,全部線程都執行到該行後,全部線程才繼續執行後面的代碼,請看例子:

1 #pragma omp parallel num_threads(6)
2 {
3     #pragma omp critical
4     std::cout << omp_get_thread_num() << " ";
5     #pragma omp critical
6     std::cout << omp_get_thread_num()+10 << " ";
7 }

1 #pragma omp parallel num_threads(6)
2 {
3     #pragma omp critical
4     std::cout << omp_get_thread_num() << " ";
5     #pragma omp barrier
6     #pragma omp critical
7     std::cout << omp_get_thread_num()+10 << " ";
8 }

能夠看到,這時一位數數字打印完了纔開始打印兩位數數字,由於,全部線程執行到第5行代碼時,都要等待全部線程都執行到第5行,這時全部線程再都繼續執行第7行及之後的代碼,即所謂同步。

    再來講說for, sections, single directives的隱含barrier,以及nowait clause以下示例:

 1 #pragma omp parallel num_threads(6)
 2 {
 3     #pragma omp for
 4     for(int i=0; i<10; ++i){
 5         #pragma omp critical
 6         std::cout << omp_get_thread_num() << " ";
 7     }
 8     // There is an implicit barrier here.
 9     #pragma omp critical
10     std::cout << omp_get_thread_num()+10 << " ";
11 }

 1 #pragma omp parallel num_threads(6)
 2 {
 3     #pragma omp for nowait
 4     for(int i=0; i<10; ++i){
 5         #pragma omp critical
 6         std::cout << omp_get_thread_num() << " ";
 7     }
 8     // The implicit barrier here is disabled by nowait.
 9     #pragma omp critical
10     std::cout << omp_get_thread_num()+10 << " ";
11 }

sections, single directives是相似的。

atomic                                        

    atomic directive保證變量被原子的更新,即同一時刻只有一個線程再更新該變量(是否是很像critical directive),見例子:

1 int m=0;
2 #pragma omp parallel num_threads(6)
3 {
4     for(int i=0; i<1000000; ++i)
5         ++m;
6 }
7 std::cout << "value should be: " << 1000000*6 << std::endl;
8 std::cout << "value is: "<< m << std::endl;

m實際值比預期要小,由於「++m」的彙編代碼不止一條指令,假設三條:load, inc, mov(讀RAM到寄存器、加1,寫回RAM),有可能線程A執行到inc時,線程B執行了load(線程A inc後的值還沒寫回),接着線程A mov,線程B inc後再mov,本來應該加2就變成了加1。

    使用atomic directive後能夠獲得正確結果:

1 int m=0;
2 #pragma omp parallel num_threads(6)
3 {
4     for(int i=0; i<1000000; ++i)
5         #pragma omp atomic
6         ++m;
7 }
8 std::cout << "value should be: " << 1000000*6 << std::endl;
9 std::cout << "value is: "<< m << std::endl;

    那用critical directive行不行呢:

1 int m=0;
2 #pragma omp parallel num_threads(6)
3 {
4     for(int i=0; i<1000000; ++i)
5         #pragma omp critical
6         ++m;
7 }
8 std::cout << "value should be: " << 1000000*6 << std::endl;
9 std::cout << "value is: "<< m << std::endl;

    差異爲什麼呢,顯然是效率啦,咱們作個定量分析:

 1 #pragma omp parallel num_threads(6)
 2 {
 3     for(int i=0; i<1000000; ++i) ;
 4 }
 5 int m;
 6 double t, t2;
 7 m = 0;
 8 t = omp_get_wtime();
 9 #pragma omp parallel num_threads(6)
10 {
11     for(int i=0; i<1000000; ++i)
12         ++m;
13 }
14 t2 = omp_get_wtime();
15 std::cout << "value should be: " << 1000000*6 << std::endl;
16 std::cout << "value is: "<< m << std::endl;
17 std::cout << "time(S): " << t2-t << std::endl;
18 m = 0;
19 t = omp_get_wtime();
20 #pragma omp parallel num_threads(6)
21 {
22     for(int i=0; i<1000000; ++i)
23         #pragma omp critical
24         ++m;
25 }
26 t2 = omp_get_wtime();
27 std::cout << "value should be: " << 1000000*6 << std::endl;
28 std::cout << "value is: "<< m << std::endl;
29 std::cout << "time of critical(S): " << t2-t << std::endl;
30 m = 0;
31 t = omp_get_wtime();
32 #pragma omp parallel num_threads(6)
33 {
34     for(int i=0; i<1000000; ++i)
35         #pragma omp atomic
36         ++m;
37 }
38 t2 = omp_get_wtime();
39 std::cout << "value should be: " << 1000000*6 << std::endl;
40 std::cout << "value is: "<< m << std::endl;
41 std::cout << "time of atomic(S): " << t2-t << std::endl;

按照慣例,須要列出機器配置:Intel Xeon Processor E5-2637 v2 (4核8線程 15M Cache, 3.50 GHz),16GB RAM。上面代碼須要在Release下編譯運行以得到更爲真實的運行時間(實際部署的程序不多是Debug版本的),第一個parallel directive的用意是跳過潛在的建立線程的步驟,讓下面三個parallel directives有相同的環境,以增長可比性。從結果能夠看出,沒有atomic clause或critical clause時運行時間短了不少,可見正確性是用性能置換而來的。不出所料,「大材小用」的critical clause運行時間比atomic clause要長不少。

flush                                        

    指示全部線程對全部共享對象具備相同的內存視圖(view of memory),該directive指示將對變量的更新直接寫回內存(有時候給變量賦值可能只改變了寄存器,後來才才寫回內存,這是編譯器優化的結果)。這很差理解,看例子,爲了讓編譯器盡情的優化代碼,須要在Release下編譯運行以下代碼

 1 int data, flag=0;
 2 #pragma omp parallel sections num_threads(2) shared(data, flag)
 3 {
 4     #pragma omp section // thread 0
 5     {
 6         #pragma omp critical
 7         std::cout << "thread:" << omp_get_thread_num() << std::endl;
 8         for(int i=0; i<10000; ++i)
 9             ++data;
10         flag = 1;
11     }
12     #pragma omp section // thread 1
13     {
14         while(!flag) ;
15         #pragma omp critical
16         std::cout << "thread:" << omp_get_thread_num() << std::endl;
17         -- data;
18         std::cout << data << std::endl;
19     }
20 }

程序進入了死循環…… 咱們的初衷是,用flag來作手動同步,線程0修改data的值,修改好了置flag,線程1反覆測試flag檢查線程0有沒有修改完data,線程1接着再修改data並打印結果。這裏進入死循環的可能緣由是,線程1反覆測試的flag只是讀到寄存器中的值,由於線程1認爲,只有本身在訪問flag(甚至覺得只有本身這1個線程),在本身沒有修改內存以前不須要從新去讀flag的值到寄存器。用flush directive修改後:

 1 int data=0, flag=0;
 2 #pragma omp parallel sections num_threads(2) shared(data, flag)
 3 {
 4     #pragma omp section // thread 0
 5     {
 6         #pragma omp critical
 7         std::cout << "thread:" << omp_get_thread_num() << std::endl;
 8         for(int i=0; i<10000; ++i)
 9             ++data;
10         #pragma omp flush(data)
11         flag = 1;
12         #pragma omp flush(flag)
13     }
14     #pragma omp section // thread 1
15     {
16         while(!flag){
17             #pragma omp flush(flag)
18         }
19         #pragma omp critical
20         std::cout << "thread:" << omp_get_thread_num() << std::endl;
21         #pragma omp flush(data)
22         -- data;
23         std::cout << data << std::endl;
24     }
25 }

這回結果對了,解釋一下,第10行代碼告訴編譯器,確保data的新值已經寫回內存,第17行代碼說,從新從內存讀flag的值。

ordered                                        

    使用在有ordered clause的for directive(或parallel for)中,確保代碼將被按迭代次序執行(像串行程序同樣),例子:

 1 #pragma omp parallel num_threads(8)
 2 {
 3     #pragma omp for ordered
 4     for(int i=0; i<10; ++i){
 5         #pragma omp critical
 6             std::cout << i << " ";
 7         #pragma omp ordered
 8         {
 9             #pragma omp critical
10                 std::cout << "-" << i << " ";
11         }
12     }
13 }

只看前面有"-"的數字,是否是按順序的,而沒有"-"的數字則沒有順序。值得強調的是for directive的ordered clause只是配合ordered directive使用,而不是讓迭代有序執行的意思,後者的代碼是這樣的:

1 #pragma omp for ordered
2 for(int i=0; i<10; ++i)
3     #pragma omp ordered{
4     ; // all the C++ for code
5 }

threadprivate                                      

    將全局或靜態變量聲明爲線程私有的。爲理解線程共享和私有變量,看以下代碼:

1 int a;
2 std::cout << omp_get_thread_num() << ": " << &a << std::endl;
3 #pragma omp parallel num_threads(8)
4 {
5     int b;
6     #pragma omp critical
7     std::cout << omp_get_thread_num() << ": " << &a << "  " << &b << std::endl;
8 }

記住第3-7行代碼要被8個線程執行8遍,變量a是線程之間共享的,變量b是每一個線程都有一個(在線程本身的棧空間)。

    怎麼區分哪些變量是共享的,哪些是私有的呢。在parallel region內定義的變量(非堆分配)固然是私有的。沒有特別用clause指定的(上面代碼就是這樣),在parallel region前(parallel region後的不可見,這點和純C++相同)定義的變量是共享的,在堆(用new或malloc函數分配的)上分配的變量是共享的(即便是在多個線程中使用new或malloc,固然指向這塊堆內存的指針多是私有的),for directive做用的C++ for的循環變量無論在哪裏定義都是私有的。

    好了,回到threadprivate directive,看例子:

 1 #include<omp.h>
 2 #include<iostream>
 3 int a;
 4 #pragma omp threadprivate(a)
 5 int main()
 6 {
 7     std::cout << omp_get_thread_num() << ": " << &a << std::endl;
 8     #pragma omp parallel num_threads(8)
 9     {
10         int b;
11         #pragma omp critical
12         std::cout << omp_get_thread_num() << ": " << &a << "  " << &b << std::endl;
13     }
14     std::cin.get();
15     return 0;
16 }

    下面是最後幾個沒有講的clauses:private, firstprivate, lastprivate, shared, default, reduction, copyin, copyprivate clauses,先看private clause:

1 int a = 0;
2 std::cout << omp_get_thread_num() << ": " << &a << std::endl;
3 #pragma omp parallel num_threads(8) private(a)
4 {
5     #pragma omp critical
6     std::cout << omp_get_thread_num() << ": *" << &a << "  " << a << std::endl;
7 }

private clause將變量a由默認線程共享變爲線程私有的,每一個線程會調用默認構造函數生成一個變量a的副本(固然這裏int沒有構造函數)。

    firstprivate clause和private clause的區別是,會用共享版本變量a來初始化。lastprivate clause在private基礎上,將執行最後一次迭代(for)或最後一個section塊(sections)的線程的私有副本拷貝到共享變量。shared clause和private clause相對,將變量聲明爲共享的。以下例子,其中的shared clause能夠省略:

 1 int a=10, b=11, c=12, d=13;
 2 std::cout << "abcd's values: " << a << " " << b << " " << c << " " << d << std::endl;
 3 #pragma omp parallel for num_threads(8) \
 4     firstprivate(a) lastprivate(b) firstprivate(c) lastprivate(c) shared(d)
 5 for(int i=0; i<8; ++i){
 6     #pragma omp critical
 7     std::cout << "thread " << omp_get_thread_num() << " acd's values: "
 8         << a << " " << c << " " << d << std::endl;
 9     a = b = c = d = omp_get_thread_num();
10 }
11 std::cout << "abcd's values: " << a << " " << b << " " << c << " " << d << std::endl;

每一個線程都對a,b,c,d的值進行了修改。由於d是共享的,因此每一個線程打印d前可能被其餘線程修改了。parallel region結束,a的共享版本不變,b,c因爲被lastprivate clause聲明瞭,因此執行最後一次迭代的那個線程用本身的私有b,c更新了共享版本的b,c,共享版本d的值取決於那個線程最後更新d。

    default(shared|none):參數shared同於將全部變量用share clause定義,參數none指示對沒有用private, shared, reduction, firstprivate, lastprivate clause定義的變量報錯。

    reduction clause用於歸約,以下是一個並行求和的例子:

 1 int sum=0;
 2 std::cout << omp_get_thread_num() << ":" << &sum << std::endl << std::endl;
 3 #pragma omp parallel num_threads(8) reduction(+:sum)
 4 {
 5     #pragma omp critical
 6     std::cout << omp_get_thread_num() << ":" << &sum << std::endl;
 7     #pragma omp for
 8     for(int i=1; i<=10000; ++i){
 9         sum += i;
10     }
11 }
12 std::cout << "sum's valuse: " << sum << std::endl;

能夠看到變量sum在parallel region中是線程私有的,每一個線程用本身的sum求一部分和,最後將全部線程的私有sum加起來賦值給共享版本的sum。除了「+」歸約,/, |, &&等均可以做爲歸約操做的算法。

    copyin clause讓threadprivate聲明的變量的值和主線程的值相同,以下例子:

 1 #include<omp.h>
 2 #include<iostream>
 3 int a;
 4 #pragma omp threadprivate(a)
 5 int main()
 6 {
 7     a = 99;
 8     std::cout << omp_get_thread_num() << ": " << &a << std::endl << std::endl;
 9     #pragma omp parallel num_threads(8) copyin(a)
10     {
11         #pragma omp critical
12         std::cout << omp_get_thread_num() << ": *" << &a << "  " << a << std::endl;
13     }
14     std::cin.get();
15     return 0;
16 }

若是第9行代碼修改成去掉copyin clause,結果以下:

    copyprivate clause讓不一樣線程中的私有變量的值在全部線程中共享,例子:

1 int a = 0;
2 #pragma omp parallel num_threads(8) firstprivate(a)
3 {
4     #pragma omp single copyprivate(a)
5     a = omp_get_thread_num()+10;
6     #pragma omp critical
7     std::cout << omp_get_thread_num() << ": *" << &a << "  " << a << std::endl;
8 }

能寫在copyprivate裏的變量必須是線程私有的,變量a符合這個條件,從上面結果能夠看出,single directive的代碼是被第4號線程執行的,雖然第4號線程賦值的a只是這個線程私有的,可是該新值將被廣播到其餘線程的a,這就形成了上面的結果。

若是去掉copyprivate clause,結果變爲:

此次single directive的代碼是被第0號線程執行的。

    呼,終於說完了,未盡事宜,見另外一篇文章:OpenMP共享內存並行編程總結表

 

6. 加速比

    加速比即同一程序串行執行時間除以並行執行時間,即並行化以後比串行的性能提升倍數。理論上,加速比受這些因素影響:程序可並行部分佔比、線程數、負載是否均衡(能夠查查Amdahl定律),另外,因爲實際執行時並行程序可能存在的總線衝突,使得內存訪問稱爲瓶頸(還有Cache命中率的問題),實際加速比通常低於理論加速比。

    爲了看看加速比隨線程數增長的變化狀況,編寫了以下代碼,須要在Release下編譯運行代碼

 1 #include<iostream>
 2 #include<omp.h>
 3 int main(int arc, char* arg[])
 4 {
 5     const int size = 1000, times = 10000;
 6     long long int data[size], dataValue=0;
 7     for(int j=1; j<=times; ++j)
 8         dataValue += j;
 9  
10     #pragma omp parallel num_threads(16)
11         for(int i=0; i<1000000; ++i) ;
12  
13     bool wrong; double t, tsigle;
14     for(int m=1; m<=16; ++m){
15         wrong = false;
16         t = omp_get_wtime();
17         for(int n=0; n<100; ++n){
18             #pragma omp parallel for num_threads(m)
19             for(int i=0; i<size; ++i){
20                 data[i] = 0;
21                 for(int j=1; j<=times; ++j)
22                     data[i] += j;
23                 if(data[i] != dataValue)
24                     wrong = true;
25             }
26         }
27         t = omp_get_wtime()-t;
28         if(m==1) tsigle=t;
29         std::cout << "num_threads(" << m << ") rumtime: " << t << " s.\n";
30         std::cout << "wrong=" << wrong << "\tspeedup: " << tsigle/t << "\tefficiency: " << tsigle/t/m << "\n\n";
31     }
32  
33     std::cin.get();
34     return 0;
35 }

能夠看到,因爲咱們的程序是在操做系統層面上運行,而非直接在硬件上運行,上面的測試結果出現了看似難以想象的結果——效率居然有時能大於1!最好的加速比出如今num_threads(8)時,爲7.4左右,已經很接近物理核心數8了,充分利用多核原來如此簡單。

相關文章
相關標籤/搜索