OpenMP共享內存並行編程詳解

1. 介紹

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

      本文對Windows上Visual Studio開發環境下的OpenMP並行編程進行簡單的探討。本文參考了維基百科關於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也足夠了。

2. 第一個OpenMP程序

實驗平臺:win7, VS2010

step 1: 新建控制檯程序

step 2: 項目屬性,所有配置下“配置屬性>>C/C++>>語言>>OpenMP支持”修改爲是(/openmp),如下圖:

step 3: 添加如下代碼:

include<omp.h>
#include<iostream>
int main()
{
    std::cout << "parallel begin:\n";
    #pragma omp parallel
    {
        std::cout << omp_get_thread_num();
    }
    std::cout << "\n parallel end.\n";
    std::cin.get();
    return 0;
 }

step 4: 運行結果如下圖:

可以看到,我的計算機是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循環。下面是一個例子:

const int size = 1000;
int data[size];
#pragma omp parallel
{
    #pragma omp for
    for(int i=0; i<size; ++i)
        data[i] = 123;
}

默認情況下,上面的代碼中,程序執行到“#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,上面代碼等價於這樣:

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

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

#pragma omp parallel num_threads(6)
{
    #pragma omp for
    for(int i=0; i<1000000; ++i)
        if(i>999)
            break;
}

編譯器報錯如下:

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:

#pragma omp parallel num_threads(3)
{
    #pragma omp for
    for(int i=0; i<9; ++i){
        #pragma omp critical
        std::cout << omp_get_thread_num() << i << " ";
    }
}

上面輸出說明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,結果如下:

        opyprivate: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了,充分利用多核原來如此簡單。

發佈了61 篇原創文章 · 獲贊 61 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章