OpenMP

平行化的開放標準

OpenMP(Open Multi-Processing)是一套支援跨平台共用主記憶體方式的多線程並行的編程API,使用C,C++Fortran語言,可以在大多數的處理器體系和作業系統中執行,包括Solaris, AIX, HP-UX, GNU/Linux, Mac OS X, 和Microsoft Windows。包括一套編譯器指令、庫和一些能夠影響執行行為的環境變數。

OpenMP
OpenMP logo
原作者OpenMP Architecture Review Board[1]
開發者OpenMP Architecture Review Board[1]
目前版本5.0(2018年11月8日,​6年前​(2018-11-08
程式語言C, C++, Fortran
作業系統跨平台
平台跨平台
類型API
許可協定多種[2]
網站http://openmp.org

OpenMP採用可移植的、可延伸的模型,為程式設計師提供了一個簡單而靈活的開發平台,從標準桌面電腦到超級電腦的並列應用程式介面。

混合併行編程模型構建的應用程式可以同時使用OpenMP和MPI,或更透明地通過使用OpenMP擴充的非共用主記憶體系統上執行的電腦叢集。

OpenMP是由OpenMP Architecture Review Board牽頭提出的,並已被廣泛接受的,用於共用主記憶體並列系統的多線程程式設計的一套指導性註釋(Compiler Directive)。OpenMP支援的程式語言包括C語言C++Fortran;而支援OpenMP的編譯器包括Sun StudioIntel Compiler,以及開放原始碼GCCLLVMOpen64編譯器。OpenMP提供了對並列演算法的高層的抽象描述,程式設計師通過在原始碼中加入專用的pragma來指明自己的意圖,由此編譯器可以自動將程式進行並列化,並在必要之處加入同步互斥以及通訊。當選擇忽略這些pragma,或者編譯器不支援OpenMP時,程式又可退化為通常的程式(一般為串行),程式碼仍然可以正常運作,只是不能利用多線程來加速程式執行。

介紹

編輯
 
多線程示意圖,其中主線程分叉出並列的執行代碼塊的一些線程。

OpenMP是一個跨平台的多線程實現,主線程(順序的執行指令)生成一系列的子線程,並將任務劃分給這些子線程進行執行。這些子線程並列的執行,由執行時環境將線程分配給不同的處理器。

要進行並列執行的代碼片段需要進行相應的標記,用預編譯指令使得在代碼片段被執行前生成線程,每個線程會分配一個id,可以通過函數(called omp_get_thread_num())來獲得該值,該值是一個整數,主線程的id為0。在並列化的代碼執行結束後,子線程join到主線程中,並繼續執行程式。

預設情況下,各個線程獨立地執行並列區域的代碼。可以使用Work-sharing constructs來劃分任務,使每個線程執行其分配部分的代碼。通過這種方式,使用OpenMP可以實現任務並列數據並列

執行時環境分配給每個處理器的線程數取決於使用方法、機器負載和其他因素。線程的數目可以通過環境變數或者代碼中的函數來指定。在C/C++中,OpenMP的函數都聲明在標頭檔omp.h中。

歷史

編輯

OpenMP Architecture Review Board (ARB)於1997年10月發佈了OpenMP for Fortran 1.0。次年的10月,發佈了C/C++的標準。2000年,發佈了Fortran語言的2.0版本,並於2002年發佈了C/C++語言的2.0版本。2005年,包含Fortran和C/C++的2.5版本發佈了。

在2008年5月發佈了3.0版。3.0中的新功能包括任務(tasks)和任務結構(task construct)的概念。這些新功能總結在OpenMP3.0規範的附錄F中。 OpenMP規範的3.1版於2011年7月9日發佈。

4.0版本在2013年7月發佈,它增加或改進以下功能:支援加速器,原子性,錯誤處理,線程關聯,任務擴充,減少用戶定義的SIMD支援和Fortran 2003的支援。

特色

編輯

OpenMP提供的這種對於並列描述的高層抽象降低了並列編程的難度和複雜度,這樣程式設計師可以把更多的精力投入到並列演算法本身,而非其具體實現細節。對基於數據分集的多線程程式設計,OpenMP是一個很好的選擇。同時,使用OpenMP也提供了更強的靈活性,可以較容易的適應不同的並列系統組態。線程粒度和負載平衡等是傳統多線程程式設計中的難題,但在OpenMP中,OpenMP庫從程式設計師手中接管了部分這兩方面的工作。

語法

編輯
#pragma omp <directive> [clause[[,] clause] ...]

directive

編輯

其中,directive共11個:

  • atomic 主記憶體位置將會原子更新(Specifies that a memory location that will be updated atomically.)
  • barrier 線程在此等待,直到所有的線程都執行到此barrier。用來同步所有線程。
  • critical 其後的代碼塊為臨界區,任意時刻只能被一個線程執行。
  • flush 所有線程對所有共用對象具有相同的主記憶體視圖(view of memory)
  • for 用在for迴圈之前,把for迴圈並列化由多個線程執行。迴圈變數只能是整型
  • master 指定由主線程來執行接下來的程式。
  • ordered 指定在接下來的代碼塊中,被並列化的 for迴圈將依序執行(sequential loop)
  • parallel 代表接下來的代碼塊將被多個線程並列各執行一遍。
  • sections 將接下來的代碼塊包含將被並列執行的section塊。
  • single 之後的程式將只會在一個線程(未必是主線程)中被執行,不會被並列執行。
  • threadprivate 指定一個變數是線程局部儲存(thread local storage)

clause

編輯

共計13個clause:

  • copyin 讓threadprivate的變數的值和主線程的值相同。
  • copyprivate 不同線程中的變數在所有線程中共用。
  • default Specifies the behavior of unscoped variables in a parallel region.
  • firstprivate 對於線程局部儲存的變數,其初值是進入並列區之前的值。
  • if 判斷條件,可用來決定是否要並列化。
  • lastprivate 在一個迴圈並列執行結束後,指定變數的值為迴圈體在順序最後一次執行時取得的值,或者#pragma sections在中,按文字順序最後一個section中執行取得的值。
  • nowait 忽略barrier的同步等待。
  • num_threads 設置線程數量的數量。預設值為當前電腦硬件支援的最大並行數。一般就是CPU的內核數目。超線程被作業系統視為獨立的CPU內核。
  • ordered 使用於 for,可以在將迴圈並列化的時候,將程式中有標記 directive ordered 的部份依序執行。
  • private 指定變數為線程局部儲存。
  • reduction Specifies that one or more variables that are private to each thread are the subject of a reduction operation at the end of the parallel region.
  • schedule 設置for迴圈的並列化方法;有 dynamic、guided、runtime、static 四種方法。
    • schedule(static, chunk_size) 把chunk_size數目的迴圈體的執行,靜態依序指定給各線程。
    • schedule(dynamic, chunk_size) 把迴圈體的執行按照chunk_size(預設值為1)分為若干組(即chunk),每個等待的線程獲得當前一組去執行,執行完後重新等待分配新的組。
    • schedule(guided, chunk_size) 把迴圈體的執行分組,分配給等待執行的線程。最初的組中的迴圈體執行數目較大,然後逐漸按指數方式下降到chunk_size。
    • schedule(runtime) 迴圈的並列化方式不在編譯時靜態確定,而是推遲到程式執行時動態地根據環境變數OMP_SCHEDULE 來決定要使用的方法。
  • shared 指定變數為所有線程共用。

OpenMP的庫函數

編輯

OpenMP定義了20多個庫函數:

1.void omp_set_num_threads(int _Num_threads);

在後續並列區域設置線程數,此呼叫只影響呼叫線程所遇到的同一級或內部巢狀級別的後續並列區域.說明:此函數只能在串行代碼部分呼叫.

2.int omp_get_num_threads(void);

返回當前線程數目.說明:如果在串行代碼中呼叫此函數,返回值為1.

3.int omp_get_max_threads(void);

如果在程式中此處遇到未使用 num_threads() 子句指定的活動並列區域,則返回程式的最大可用線程數量.說明:可以在串行或並列區域呼叫,通常這個最大數量由omp_set_num_threads()或OMP_NUM_THREADS環境變數決定.

4.int omp_get_thread_num(void);

返回當前線程id.id從1開始順序編號,主線程id是0.

5.int omp_get_num_procs(void);

返回程式可用的處理器數.

6.void omp_set_dynamic(int _Dynamic_threads);

啟用或禁用可用線程數的動態調整.(預設情況下啟用動態調整.)此呼叫只影響呼叫線程所遇到的同一級或內部巢狀級別的後續並列區域.如果 _Dynamic_threads 的值為非零值,啟用動態調整;否則,禁用動態調整.

7.int omp_get_dynamic(void);

確定在程式中此處是否啟用了動態線程調整.啟用了動態線程調整時返回非零值;否則,返回零值.

8.int omp_in_parallel(void);

確定線程是否在並列區域的動態範圍內執行.如果在活動並列區域的動態範圍內呼叫,則返回非零值;否則,返回零值.活動並列區域是指 IF 子句求值為 TRUE 的並列區域.

9.void omp_set_nested(int _Nested);

啟用或禁用巢狀並列操作.此呼叫只影響呼叫線程所遇到的同一級或內部巢狀級別的後續並列區域._Nested 的值為非零值時啟用巢狀並列操作;否則,禁用巢狀並列操作.預設情況下,禁用巢狀並列操作.

10.int omp_get_nested(void);

確定在程式中此處是否啟用了巢狀並列操作.啟用巢狀並列操作時返回非零值;否則,返回零值.

互斥鎖操作 巢狀鎖操作 功能

11.void omp_init_lock(omp_lock_t * _Lock); 12. void omp_init_nest_lock(omp_nest_lock_t * _Lock);

初始化一個(巢狀)互斥鎖.

13.void omp_destroy_lock(omp_lock_t * _Lock); 14.void omp_destroy_nest_lock(omp_nest_lock_t * _Lock);

結束一個(巢狀)互斥鎖的使用並釋放主記憶體.

15.void omp_set_lock(omp_lock_t * _Lock); 16.void omp_set_nest_lock(omp_nest_lock_t * _Lock);

獲得一個(巢狀)互斥鎖.

17.void omp_unset_lock(omp_lock_t * _Lock); 18.void omp_unset_nest_lock(omp_nest_lock_t * _Lock);

釋放一個(巢狀)互斥鎖.

19.int omp_test_lock(omp_lock_t * _Lock); 20.int omp_test_nest_lock(omp_nest_lock_t * _Lock);

試圖獲得一個(巢狀)互斥鎖,並在成功時放回真(true),失敗是返回假(false).

21.double omp_get_wtime(void);

取得wall clock time,返回一個double的數,表示從過去的某一時刻經歷的時間,一般用於成對出現,進行時間比較. 此函數得到的時間是相對於線程的,也就是每一個線程都有自己的時間.

22.double omp_get_wtick(void);

得到clock ticks的秒數.

例子

編輯

omp parallel 段內的程式碼由多線程來執行:

 int main(int argc, char* argv[])
 {
  #pragma omp parallel  
   printf("Hello, world.\n");

   return 1;
 }

執行結果

編輯
% gcc omp.c (由單線程來執行)
% ./a.out
Hello, world.

% gcc -fopenmp omp.c (由多線程來執行)
% ./a.out
Hello, world.
Hello, world.
Hello, world.
Hello, world.

環境變數

編輯

OpenMP可以使用環境變數 OMP_NUM_THREADS以控制執行線程的數量。

例子

編輯
% gcc -fopenmp omp.c 

% setenv OMP_NUM_THREADS 2(由2線程來執行)
setenv是CSH的指令

在bash shell 環境中 要用export 
% export OMP_NUM_THREADS=2 (由2線程來執行)

% ./a.out
Hello, world.
Hello, world.

優點和缺點

編輯

優點

  • 可移植的多線程代碼(在C/C++和其他語言中,人們通常為了獲得多線程而呼叫特定於平台的原語)
  • 簡單,沒必要象MPI中那樣處理訊息傳遞
  • 數據分佈和分解由指令自動完成
  • 增量並列,一次可以只在代碼的一部分執行,對代碼不需要顯著的改變
  • 統一的順序執行和並列執行的代碼,在順序執行編譯器上,OpenMP的執行按照註釋進行對待;
  • 在一般情況下,使用OpenMP並列時原始的(串行)代碼陳述式不需要進行修改,這減少不經意間引入錯誤的機會。
  • 同時支援粗粒度和細粒度的並列
  • 可以在GPU上使用[3]

缺點

  • 存在引入難以除錯的同步錯誤和競爭條件的風險
  • 目前,只能在共用主記憶體的多處理器平台高效執行
  • 需要一個支援OpenMP的編譯器
  • 可延伸性是受到主記憶體架構的限制
  • 不支援比較和交換
  • 缺乏可靠的錯誤處理
  • 缺乏對線程與處理器對映的細粒度控制
  • 很容易出現一些不能共用的代碼
  • 多線程的可執行檔案的啟動需要更多的時間,可能比單線程的執行的慢,因此,使用多線程一定要有其他有優勢的地方
  • 很多情況下使用多線程不僅沒有好處,還會帶來一些額外消耗

爭議

編輯

作為高層抽象,OpenMP並不適合需要複雜的線程間同步和互斥的場合。 OpenMP的另一個缺點是不能在非共用主記憶體系統(如電腦叢集)上使用。在這樣的系統上,MPI使用較多。

編譯器支援

編輯

主流C/C++編譯器,如gcc與visual C++,都內在支援OpenMP。一般都必須在程式中#include <omp.h>

gcc編譯時需使用編譯選項-fopenmp。但是,如果編譯為目標文件與連結生成可執行檔案是分開為兩步操作,那麼連結時需要給出附加庫gomp,否則會在連結時報錯「undefined reference to `omp_get_thread_num'"。

Visual C++需要在IDE的編譯選項->語言->支援OpenMP。這實際上使用了編譯選項/openmp


參見

編輯

參考文獻

編輯
  1. ^ 1.0 1.1 About the OpenMP ARB and. OpenMP.org. 2013-07-11 [2013-08-14]. (原始內容存檔於2013-08-09). 
  2. ^ OpenMP Compilers. OpenMP.org. 2013-04-10 [2013-08-14]. (原始內容存檔於2013-07-17). 
  3. ^ OpenMP Accelerator Support for GPUs. [2019-12-03]. (原始內容存檔於2019-12-03). 

外部連結

編輯