1. 介紹
並行計算機可以簡單分爲共享內存和分佈式內存,共享內存就是多個核心共享一個內存,目前的PC就是這類(不管是隻有一個多核CPU還是可以插多個CPU,它們都有多個核心和一個內存),一般的大型計算機結合分佈式內存和共享內存結構,即每個計算節點內是共享內存,節點間是分佈式內存。想要在這些並行計算機上獲得較好的性能,進行並行編程是必要條件。目前流行的並行程序設計方法是,分佈式內存結構上使用MPI,共享內存結構上使用Pthreads或OpenMP。我們這裏關注的是共享內存並行計算機,因爲編輯這篇文章的機器就屬於此類型(普通的臺式機)。和Pthreads相比OpenMP更簡單,對於關注算法、只要求對線程之間關係進行最基本控制(同步,互斥等)的我們來說,OpenMP再適合不過了。
本文對Windows上Visual Studio開發環境下的OpenMP並行編程進行簡單的探討。本文參考了維基百科關於OpenMP條目、OpenMP.org(有OpenMP Specification)、MSDM上關於OpenMP條目以及教材《MPI與OpenMP並行程序設計(C語言版)》:
- http://zh.wikipedia.org/wiki/OpenMP
- http://openmp.org/
- http://msdn.microsoft.com/en-us/library/tt15eb9t(v=vs.100).aspx
- 《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的多次迭代如何在多個線程間劃分:
- 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等於迭代次數除以線程數,即將迭代分成連續的和線程數相同的等分(或近似等分)。
- schedule(dynamic, size)同樣分組,然後依次將每組分給目前空閒的線程(故叫動態)。
- schedule(guided, size) 把迭代分組,分配給目前空閒的線程,最初組大小爲迭代數除以線程數,然後逐漸按指數方式(依次除以2)下降到size。
- 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了,充分利用多核原來如此簡單。