OpenMP 心得 (一)

最近幾年多核心 CPU 已蔚為流行,四核以上的 CPU 不再是少數追求高效能的電腦玩家才會選擇的昂貴零組件,而是漸漸成為一般電腦的標準配備,可以想見再過幾年在市面上出現十幾、三十幾核的桌上型 CPU 也不是不可能的事。多核心的 CPU 代表電腦擁有可以同時處理多項工作的能力,所以我們可以一次執行數個程式來獲得較佳的工作量對時間之效益比,或者是將單一工作分解成數個可獨立執行的部份 來平行處理以縮短該工作的執行時間。後者便是平行程式發展的著眼點。早期有需要寫平行程式的人大多是屬於比較專業的研究人員,也只有這些人所屬的機構單位 才有經費去設置高價的多 CPU 電腦系統。但是在多核心電腦普及的現在,一般程式開發人員也開始有需要去接觸這方面的知識,畢竟好的程式設計師就是要在許可的範圍內盡量去榨取電腦系統的 資源,只使用一個核心去執行程式卻放著其它的核心在旁邊納涼對某些程式設計師而言根本是一種罪惡。

程式平行化的限制與代價
目前常見的程式語言如 C/C++、Java、Fortran 等都是屬於序列執行的語言,即程式執行的步驟是按照程式碼設計的流程依序執行,但是透過適當的 API 我們可以將這些語言寫成的程式加以平行化。當然將程式平行化是有些限制與代價存在,例如

  1. 程式中只有彼此不相關的敘述以及可以獨立執行的任務才能被平行化。
  2. 程式碼必須經過適當的修改才能被平行執行。
  3. 選用的 API 與實際執行程式的平臺架構有關。

第一點是說如果兩個敘述 (或任務) 是彼此相關的,例如敘述 (或任務) B 需要敘述 (或任務) A 的計算結果,那麼即使我們將這兩個敘述 (或任務) 交由不同的處理單元來執行,B 也必須等到 A 完成後才能運作,這樣就不是平行執行而是序列執行。

第二點指的是我們必須對程式碼中需要平行執行的部分再多下點功夫,像是如何將程序任務切割、建立執行緒去執行被分割的任務、或在不同執行程序間交換傳遞訊 息等,這些功夫依程式人員採用的平行化 API 而有不同的做法與難易度,有的 API 提供若干函式來處理這些工作,使用者必須明確的在要平行化的程式區段中調用這些 API 函式以達成平行執行的目的;而有些 API 則是以特殊的程式敘述或註解形式,將其放在程式碼要平行化的區段,然後透過這些敘述指導編譯器在編譯階段將該區段設定為平行執行。

第三點指的是決定程式如何平行處理是依執行程式的平臺架構而定。目前常見的平行處理平臺架構有共享記憶體架構 (shared memory)、分散記憶體架構 (distributed memory)、與異質裝置架構 (heterogeneous device) 三種。第一種共享記憶體架構是目前最常見的平行執行架構,所有配備多核心 CPU 的機器都屬於這種架構,應用在這種架構的平行化程式以多執行緒 (multi-thread) 型式為主流,一般使用的 API 為 Pthreads 與 OpenMP;分散記憶體架構則應用在叢集電腦上,其平行程式是屬於多程序 (multi-process) 型式,程序之間的訊息是透過網路來傳遞,常用的平行化 API 為 MPI (Message Passing Interface);異質裝置結構指的是用不同型式的計算裝置所組成的平臺,像是近年開始流行使用顯示卡做計算的 GPGPU 運算即是屬於此類,較出名的 API 有 CUDA、OpenCL、與 DirectCompute 等。這些架構的分界現今來看並不是那麼的壁壘分明,目前好一點的 PC 可能就是第一種與第三種的混合體,如果有兩臺以上這樣的機器透過區域網路連在一起再加上適當的設定就成為同時具備上述三種架構的叢集電腦。

程式平行化是縮短程式執行時間的萬靈丹嗎?
這個問題會依程式本身的結構、使用的 API、以及用於跑程式的硬體設備而有不同的答案,有時候我們會發現經過平行化的程式反而執行得比原本序列程式來得慢。其中一個很重要的因素是因為同樣的 程式在平行執行時會比其在序列執行時多出一些像是分配任務、交換資訊之類的額外工作,如果這些額外工作的開銷加起來比程序被平行執行時所獲得的時間縮減效 益還大的話就會拖慢整體工作完成的時間。因此建立比較的基準 (benchmark) 是一件相當重要的事,通常比較的基準就是該程式的序列執行版本。當我們將程式平行化後一定要跟 benchmark 做比較,看平行執行是否真的划得來。

程式平行化要考量的問題
1. 能獲得多少加速的效益:一個程式是否該被平行化必須要考慮到它可被平行化的部份在整體執行中所佔的時間比例。例如一個程式只有 20% 的執行時間部分可以被平行化,在最佳的理想狀態下我們可以將這 20% 的部分縮減到幾近於 0,但即使如此也只是獲得整體加速 1.25 倍的效益而已,如果這個程式的執行時間本來就不長,那 1.25 倍的加速就顯得更舉無輕重。

2. 要花多少時間在修改程式上:我們都希望花在修改程式的時間是越短越好,而修改程式的難易度除了取決於撰碼者的經驗外也跟採用的 API 有很大的關係。魚與熊掌不可兼得,通常越低階的 API 如 Pthreads、MPI 等可以提供比較好的平行效能,但代價是要花比較多的工夫來修改程式;而高階的 API 如 OpenMP 的性能則取決於使用的編譯器好壞,但具有簡單易學及快速修改程式平性化的長處。

OpenMP 簡介
OpenMP 是應用在開發 shared memory 架構的平行程式 API,目前官方預設支援的程式語言有 C/C++ 與 Fortran。它使用特殊形式的前置敘述或註解來指導編譯器將特定的程式區塊變成平行執行,使用者幾乎不用修改程式本身架構即能完成程式的平行化。 OpenMP 由三種功能所組成,分別是 directives、library functions、以及 environment variables。接下來將對這三項功能進行說明。

1. 程式平行化的方式是由 directive 來決定。我們在特定形式起始的敘述中加入代表不同平行功能的 directive 關鍵字來決定程式該如何被平行化。除了特殊的複合構造外,一般一個 OpenMP 平行敘述只能有一個 directive 存在,但是可以在它之後加上一些 clauses 來對平行方式做更進一步的設定,兩個以上的 clauses 之間以逗號作區隔。Directive 的通用表達語法如下:

  • C/C++: #pragma omp <directive> [clause[[, ]clause] ...]
  • Fortran:!$omp <directive> [clause[[, ]clause] ...]

舉一個用於平行 C/C++ for 迴圈結構的例子:
        #pragma omp for private(i, j, k)
其中 #pragma omp (Fortran 為 !$omp) 是用來告訴編譯器這是屬於 OpenMP 敘述的特殊樣式,接下來的 for 為用於 平行化 for loop 結構的 directive,編譯器會對緊接在此 OpenMP 敘述的 for 迴圈進行平行化。private 屬於 clause 的一種,用來設定迴圈內 i、j、k 三個變數的存取範圍為各執行緒所私有的。另外要注意的是 C/C++ 的 OpenMP 敘述形式只能用英文小寫來表達,而 Fortran 則無大小寫之分。

2. 使用 OpenMP 來平行化程式並不一定要用到它提供的 library functions,但是這些函式有蠻多實用的功能來協助使用者控制程式或取得一些資訊。例如 omp_get_num_threads() 函式會回傳程式執行時用了多少個執行緒,omp_get_thread_num() 則回傳目前執行程序的執行緒編號。要使用 OpenMP 的 library functions 時必須在程式中明確地匯入 OpenMP 函式庫,例如:

  • C/C++: #include <omp.h>
  • Fortran:use omp_lib

3. 在平行程式執行前我們可以先設定好一些環境參數來控制平行程式的運作。例如預設上 OpenMP 程式在執行時會用盡系統可用的 CPU 來執行程式,但我們可以在程式執行前以環境變數 OMP_NUM_THREADS 來設定要使用多少個執行緒來平行執行程式。以 bash shell 為例,設定四個執行緒的方式為在 shell 下輸入以下指令:
        export OMP_NUM_THREADS=4


編譯器與編譯選項
目前大部分的 C/C++ 與 Fortran 編譯器都能支援 OpenMP,只要在編譯程式時加入 OpenMP 的選項,即會在編譯過程中依程式碼內的 directives 將特定程式區塊進行平行化。例如 gcc 與 gfortran 中 OpenMP 的選項為 -fopenmp;而 Intel C/C++ 與 Intel Fortran 編譯器的 OpenMP 選項為 -openmp。以下為編譯範例:

  • for GCC compiler:gcc -fopenmp example.c -o a.out
  • for Intel Fortran compiler: ifort -openmp example.f -o a.out

轉載至 aaz的記憶倉庫

創作者介紹
創作者 忽倫 的頭像
忽倫

一段輝煌的回憶,一個冬日的故事

忽倫 發表在 痞客邦 留言(0) 人氣()