參考自TAMU的PPThttps://people.math.umass.edu/~johnston/PHI_WG_2014/OpenMPSlides_tamu_sc.pdf
什么是OpenMP
在C、C++和FORTRAN中用於編寫共享內存並行程序的事實上的標准API
OpenMP API 由以下組成:
-
編譯器指令Compiler Directives
- 運行時 子程序/函數 Runtime subroutines/functions
- 環境變量 Environment variables
例子:
代碼
PROGRAM HELLO
!$OMP PARALLEL
PRINT *,”Hello World”
!$ OMP END PARALLEL
STOP
END
編譯指令
intel: ifort -openmp -o hi.x hello.f pgi: pgfortran -mp -o hi.x hello.f gnu: gfortran -fopenmp -o hi.x hello.f
運行指令
Export OMP_NUM_THREADS=4
./hi.x
FORTRAN指令格式:
!$ OMP PARALLEL [clauses]
:
!$OMP END PARALLEL
OpenMP 遵循Fork/Join模型
OpenMP程序從一個線程開始;主線程(線程0)
在並行區域開始時,master創建一組並行“worker”線程(FORK)
並行塊中的語句由每個線程並行執行
在並行區域的末尾,所有線程同步(隱式屏障implicit barrier),並連接主線程(JOIN)
OpenMP線程與內核
- 線程是獨立的程序代碼執行序列
- 代碼塊只有一個入口和一個出口
- 與核心/CPU無關
- OpenMP的線程們都映射到物理核心們上
- 可以在一個核心上映射多個線程
- 實際上,線程和核心最好是一對一的映射。
OpenMP的線程數可通過如下方法設置:
-
環境變量 OMP_NUM_THREADS
-
運行時函數 omp_set_num_threads(n)
其它獲取線程信息的有用的函數:
- 運行時函數 omp_get_num_threads()
- 返回並行域中線程數目
- 如果在並行域外返回1
- 運行時函數 omp_get_thread_num()
- 返回組中線程id
- 值為[0,n-1],其中n為線程總數
- 主線程的id是0
共享和私有變量
OpenMP在OpenMP塊內,提供了一個聲明變量是私有private還是共享shared的方法。這通過遵循如下OpenMP條款實現:
SHARED(list)
- 在list中的所有變量被認為是共享的
- 每一個openmp線程都可以訪問所有這些變量
PRIVATE(list)
- 每一個openmp線程,對於list中的變量,都有一個自己“私有的”副本
- 其它的openmp線程不能訪問這個“私有的”副本
例如 !$OMP PARALLEL PRIVATE(a,b,c) (fortran)
在OpenMP中,默認情況下,大多數變量都認為是共享的。以下情況例外:指針變量(Fortran, C/C++)和在parallel region區內聲明的變量(C/C++)
TIPS:實際問題
- openmp為每個工作線程創建單獨的數據堆棧以存儲私有變量的副本(主線程使用常規堆棧)
- OpenMP標准未定義這些堆棧的大小
- 英特爾編譯器:默認堆棧為4MB
- gcc/gfortran:默認堆棧為2mb
- 超出堆棧空間時,程序的行為未定義
- 盡管大多數編譯器/RT都會拋出seg fault
- 要增加堆棧大小,請使用環境變量OMP_STACKSIZE,例如
- export OMP_STACKSIZE=512M
- export OMP_STACKSIZE=1GB
- 確保主線程有足夠大的堆棧空間使用
- ulimit-s命令(unix/linux)
作業共享(手動方法)
到目前為止,只討論了在所有並行區域中做同樣的工作,這不是很有用。我們想要的是在所有線程之間共享工作,這樣我們就能更快地解決問題。
作業共享(OpenMP 方法)
OpenMP幫你划分好了迭代空間。你唯一需要做的就是添加!$OMP DO 和!$OMP END DO指令
“可以通過使用 !$OMP PARALLEL DO 指令 使得命令更加緊湊”
規約 Reductions
或者
a = a 運算符 表達式 稱為歸約運算。顯然,變量“a”帶有一個 流依賴關系(稍后將討論),它阻礙了了並行化。
對於此類情況,openmp提供了規約語句 REDUCTION(op:list)。語句適用於滿足下列限制條件時:
- a是列表中的標量變量
- expr是一個標量表達式,它不引用a
- 只允許某些類型的運算符;例如,+,*,。-
- 在fortran中,運算符也可以是內置函數的;例如MAX,MIN,IOR
- 列表中的變量必須是共享的
提示:openmp作用域規則
到目前為止,所有指令都嵌套在同一個例程中(最外層的!$OMP PARALLEL)。然而,OpenMP提供了更靈活的范圍規則。它只允許有例行程序!$OMP DO在這種情況下,我們調用!$OMP DO執行孤立指令orphaned directive。
注意:有一些規則(例如,當遇到一個!$OMP DO directive,程序應在parallel部分)
OMP如何安排迭代?
盡管openmp標准沒有指定循環應該如何分區,但默認情況下,大多數編譯器在N/p(N 迭代數,p線程數)塊中分割循環。這稱為靜態調度(塊大小為N/p)
為了顯式地告訴編譯器使用靜態調度(或者我們稍后將看到的其他調度),openmp提供了SCHEDULE子句
$!OMP DO SCHEDULE (STATIC,n) (n是分塊的大小)
靜態調度的問題
在靜態調度中,迭代次數在所有openmp線程中均勻分布(即每個線程將被分配相似的迭代次數)。這並不總是分割的最佳方式。這是為什么?
動態調度
有了動態調度,新的塊在線程可用時被分配給它們。OpenMP提供兩個動態調度:
- $!OMP DO SCHEDULE(DYNAMIC,n) // n is chunk size
循環迭代被分成大小塊。當一個線程完成一個塊時,它被動態地分配給另一個塊。
- $!OMP DO SCHEDULE(GUIDED,n) // n is chunk size
類似於DYNAMIC,但塊大小是相對於剩余迭代次數的
請記住:雖然在某些情況下,動態調度可能是防止負載不平衡的首選,但與靜態調度相比,這涉及到很大的開銷。
OMP SINGLE
openmp提供的另一個共享作業指令是!$OMP SINGLE。當遇到單個指令時,只有一個團隊成員將執行塊中的代碼
- 一個線程(不一定是主線程)執行塊
- 其他線程將等待
- 對線程不安全代碼有用
- 對I/O操作有用
數據的依賴
並非所有的循環都可以並行化。在添加openmp指令之前,需要檢查是否存在任何依賴項:
我們將依賴項分為三類:
- 流依賴性:在寫之后讀Read after Write (RAW)
- 反依賴:讀后寫Write after Read (WAR)
- 輸出依賴性:寫后寫Write after Write (WAW)
對於我們的目的(openmp並行循環),我們只關心循環攜帶的依賴(循環的不同迭代中指令之間的依賴)
1. S3→S2 anti(B) 變量B反依賴。對於變量B,寫第i個的數據(S2),讀第i+1個的數據(S3) (先寫后讀)
2. S3→S4 flow(A) 變量A流依賴。對於變量A,寫第i+1個的數據(S3),讀第i個的數據(S4)(先讀后寫)
3. S4→S2 flow(B) 變量temp流依賴。對於變量temp,先讀取它的數據(S2),然后再寫入數據(S4)(先讀后寫)
4. S3→S4 flow(B) 變量temp輸出依賴。對於變量temp,先向它寫入數據(S4),然后向它寫入數據(下一個S4)(寫后寫)
循環攜帶的反依賴項和輸出依賴項不是真正的依賴項(重復使用相同的名稱),在許多情況下可以相對容易地解決。
流依賴項是真正的依賴項(有一個從定義到使用的流),在許多情況下不容易刪除。可能需要重寫算法(如果可能)
處理反/輸出依賴
使用 PRIVATE語句: 見hello_threads (略)
變量重命名(如果可能):
示例:原地左移
除了openmp之前描述的子句之外,一些非常有用的附加datascope子句:
- FIRSTPRIVATE ( list ):
與private相同,但變量“x”的每個私有副本都是用“x”的原始值(在omp區域開始之前)初始化 - LASTPRIVATE ( list ):
與private相同,但列表中上次工作共享的變量的私有副本將復制到共享版本。與!$OMP DO指令一起使用。 - DEFAULT (SHARED | PRIVATE | FIRSTPRIVATE | LASTPRIVATE ):
指定omp區域中所有變量的默認范圍。
例子學習:去除流依賴
Y=prefix(X) 前綴和(數列前n項的和)
求法:
串行:
Y[1]=X[1] Do i=2,n Y[i]=Y[i-1]+X[i] END DO
但這種算法不能用於並行運算
改寫算法
第一步:將X根據線程數分割,每一個線程計算自己的前綴和。
第二步:創建數組T,收集各個分隔段的最后一個元素,並計算前綴和到T。
第三步:每一個線程的結果的加上T[theadid]。
第四部:完成。
前綴和的實現Prefix Sum Implementation
三個獨立步驟
- 步驟1和3可以並行完成
- 步驟2必須按順序執行
- 步驟1必須在步驟2之前執行
- 步驟2必須在步驟3之前執行
注意:為了便於說明,我們可以假設數組長度是線程數目的倍數
這個案例研究展示了一個具有實際(流)依賴關系的算法示例
- 有時我們可以重寫algorightm來並行運行
- 大多數時候這不是小事
- 加速(通常)不那么令人印象深刻
OpenMP Sections
假設您有可以並行執行的代碼塊(即沒有依賴項)。要並行執行它們,openmp提供了!$OMP SECTIONS指令。語法如下:
這將並行執行“WROK1”和“WORK2”
NOWAIT 指令
每當作業共享結構中的線程(例如!$OMP DO)比其他線程更快地完成工作,它將等待所有參與線程完成各自的工作。所有線程將在作業共享結構結束時同步。
對於不需要或不想在末尾同步的情況,OpenMP 提供了NOWAIT 指令
關於作業共享總結
我們討論了OMP 作業共享結構
- !$OMP DO
- !$OMP SECTIONS
- !$OMP SINGLE
可與這些結構一起使用的有用指令(不完整列表,並非所有指令都可與每個指令一起使用)
- SHARED (list)
- PRIVATE (list)
- FIRSTPRIVATE (list)
- LASTPRIVATE(list)
- SCHEDULE (STATIC | DYNAMIC | GUIDED, chunk)
- REDUCTION(op:list)
- NOWAIT
同步化
OpenMP程序使用共享變量進行通信。我們需要確保不同的線程不會同時訪問這些變量(會導致爭用情況,為什么?)。OpenMP提供了許多同步指令。
- !$OMP MASTER
- !$OMP CRITICAL
- !$OMP ATOMIC
- !$OMP BARRIER
!$OMP MASTER
此指令確保只有主線程超出塊中的指令。沒有隱式屏障,因此其他線程不會等待master完成
與!$OMP SINGLE DIRECTIVE 有什么不同?
!$OMP CRITICAL
此指令確保只有一個線程可以執行塊中的代碼。如果另一個線程到達臨界區,它將等待直到當前線程完成此臨界區。每個線程都將執行關鍵塊,並且它們將在CRITICAL部分的末尾同步。
- 引入開銷
- 序列化關鍵塊
- 如果臨界區的時間相對較大→加速可忽略不計
!$OMP ATOMIC
這個指令非常類似於上一張幻燈片上的 !$OMP CRITICAL 指令。不同的是 !$OMP ATOMIC僅用於更新內存位置。有時 !$OMP ATOMIC被稱為mini 臨界區。
- 塊僅由一個語句組成
- 原子語句必須遵循特定語法
- 在前面的示例中,可以將“critical”替換為“atomic”
!$OMP BARRIER
!$OMP BARRIER 將強制每個線程在該屏障處等待,直到所有線程都達到該屏障為止。!$OMP BARRIER 可能是最著名的同步機制;顯式或隱式地。我們之前討論過的以下omp指令包含一個隱式屏障:
- !$ OMP END PARALLEL
- !$ OMP END DO
- !$ OMP END SECTIONS
- !$ OMP END SINGLE
- !$ OMP END CRITICAL
潛在的加速
理想情況下,我們希望有完美的加速(即使用n個處理器時n的加速)。然而,這在大多數情況下並不切實可行,原因如下:
- 並非所有的執行時間都花在並行區域(例如循環)上
例如,並非所有循環都是並行的(數據依賴關系) - 使用openmp線程有一個固有的開銷
讓我們看一個例子,它顯示了由於程序的非並行部分,加速將如何受到影響(略)
TIP: IF 指令
OpenMP提供了另一個有用的指令,用於在運行時確定並行區域是實際並行運行(多線程)還是僅由主線程運行:
IF (logical expr)
例如:
$!OMP PARALLEL IF(n > 100000) (fortran)
#pragma omp parallel if (n>100000) (C/C++)
這將只在n>100000時運行並行區域
Amdahl法則
每個程序由兩部分組成::
- 串行部分
- 並行部分
顯然,無論有多少個處理器,串行部分只在一個處理器執行。假設,程序花費總時間的p(0<p<1)部分在並行區域,(相對於串行的)運行時間將是:
((1-p)+p/N)/1
這意味着加速倍數將是:
1/((1-p)+p/N)
例如:假設80%的程序可以並行執行,且有用不限制的CPU數目,則最大加速將僅僅是5倍
OpenMP開銷/可擴展性
啟動並行OpenMP 區域不是免費的。這涉及相當多的開銷。在循環周圍放置openmp pragmas之前,請考慮以下內容:
- 記住阿姆達定律
- 盡可能並行化大多數外部循環(在某些情況下,即使迭代次數較少)
- 確保並行區域的加速足以克服開銷
循環中的迭代次數足夠大嗎?
每次迭代的工作量是否足夠? - 不同機器/操作系統/編譯器的開銷可能不同
OpenMP 程序的伸縮性並不總是很好。即,當openmp線程數增加時,加速將受到以下影響
- 更多線程競爭可用帶寬
- 緩存問題
嵌套並行
OpenMP允許嵌套並行(例如 在!$OMP DO里面嵌套!$OMP DO)
- 設置環境變量OMP_NESTED以啟用/禁用嵌套
- omp_get_nested(), omp_set_nested() 運行時函數
- 編譯器仍然可以選擇序列化嵌套的並行區域(即只使用一個線程的team )
- 大量開銷。為什么?
- 會導致額外的負載不平衡。為什么?
OpenMP循環折疊
當您擁有完全嵌套的循環時,可以使用OpenMP 命令 collapse(n)來折疊內部循環。折疊循環:
- 循環需要完全嵌套perfectly nested
- 循環需要有矩形迭代空間
- 使迭代空間更大
- 比嵌套的並行循環需要更少的同步
例子:
!$OMP PARALLEL DO PRIVATE (i,j) COLLAPSE(2) DO i = 1,2 DO j=1,2 call foo(A,i,j) ENDDO ENDDO !$OMP END PARALLEL DO
提示:使用的線程數
OpenMP有兩種模式來確定在並行區域中實際使用的線程數:
- 動態模式DYNAMIC MODE
- 每個並行區域使用的線程數可能不同
- 設置線程數只設置最大線程數(實際數量可能更少)
- 靜態模式STATIC MODE
- 線程數是固定的,由程序員決定
- 通過改變環境變量 OMP_DYNAMIC 來設置模式
- 運行時函數 omp_get_dynamic/omp_set_dynamic
數學庫
數學庫具有許多函數的非常專業化和優化版本,其中許多函數已使用OpenMP並行化。在EOS上,我們有英特爾數學內核庫(MKL)
有關mkl的更多信息:
http://sc.tamu.edu/help/eos/mathlib.php
因此,在實現自己的OpenMP數學函數之前,請檢查MKL中是否已經有一個版本