自從在 2021 年、Visual Studio 2019 開始引入 OpenMP LLVM 後,微軟終於開始有慢慢地開始針對 OpenMP 的標準支援進行更新了。
這次官方的公告是《MSVC OpenMP Update》,就是在介紹 Visual Studio 2022 17.4 中針對 C++ OpenMP 的更新了!官方的說法,是他們正在讓 Visual C++ 更符合 OpenMP 3.1 的標準。
這次更新的內容主要是:
- 支援 OpenMP 3.1 的 #pragma omp atomic
- 支援 min / max reduction
- 支援指標型變數的 #pragma omp for
其實裡面有第一項和第三項似乎在 Visual Studio 2022 17.3 的時候,在比較細的更新項目就有提到了?不知道為什麼到現在來額外比較詳細地介紹、並算到 17.4 的更新?
總之,目前 Visual C++ 的 OpenMP 更新應該都還是基於 LLVM 11 版來開發,目前也有計畫繼續更新,要建置專案的時候還是要加上 /openmp:llvm 這個參數的。
另外也要注意的,是 LLVM 基礎的 OpenMP 的 DLL 檔案,目前還是放在「debug_nonredist」的資料夾下(路徑的形式:「C:\Program Files\Microsoft Visual
Studio\2022\Enterprise\VC\Redist\MSVC\14.34.31931\debug_nonredist\x64\Microsoft.VC143.OpenMP.LLVM」),代表安裝可轉發套件可能還是不會包含需要的必要的 libomp140.x86_64.dll 這個檔案,這點是要發布程式時可能要注意的。
下面則是一些新的支援項目的介紹。
使用指標作為迴圈的索引值
最簡單的 OpenMP 程式基本上就是針對整數型別的索引值回圈來進行平行化(參考),下面就是一個簡單的例子:
#include <iostream> int main() { int aData[10] = {0,1,2,3,4,5,6,7,8,9}; #pragma omp parallel for for (int i = 0; i < 10; ++i) { std::cout << i << std::endl; } return 0; }
而現在這邊則是可以不使用 i 來做為存取的索引,而是直接用指標;程式碼會變成下面的樣子:
#include <iostream> int main() { int aData[10] = {0,1,2,3,4,5,6,7,8,9}; int *pBegin = &aData[0], *pEnd = &aData[10]; #pragma omp parallel for for (int* p = pBegin; p < pEnd; ++p) { std::cout << *p << std::endl; } return 0; }
Heresy 自己是沒想到什麼時候會需要這樣寫啦…
另外,目前還不支援針對 iterator 做 for 迴圈的平行化,但是有在規劃中。
reduction 支援 min 與 max
Visual C++ 在以前還只支援 OpenMP 2.0 的時候就有支援 reduction、提供各執行序的變數彙整功能了(參考);而這次則是加入了取最大值和最小值的功能。
以往如果要用 OpenMP 來找最大值的話,可能得用上 critical,大致上會是:
#include <array> #include <iostream> int main() { std::array aData = {0,1,2,3,4,5,6,7,8,9}; int iMaxval = INT_MIN; #pragma omp parallel for shared(iMaxval) for (int i = 0; i < aData.size(); ++i) { #pragma omp critical if (aData[i] > iMaxval) iMaxval = aData[i]; } std::cout << iMaxval << std::endl; return 0; }
而如果透過 reduction 的話,則可以寫成:
int iMaxval = INT_MIN; #pragma omp parallel for reduction(max : iMaxval) for (int i = 0; i < aData.size(); ++i) { if (aData[i] > iMaxval) iMaxval = aData[i]; }
理論上這樣的效能會比使用 critical 的時候好上許多的。
atomic 支援 OpenMP 3.1
OpenMP 的 atomic 是用來要求系統在平行化的狀況下,一次只有一個執行序來執行特定的命令用的;相較於通用性的 critical,atomic 僅支援有限度的簡單運算,所以編譯器可以提供更好的最佳化、提供更好的效能(參考)。
下面就是一個簡單的例子:
#include <iostream> int main() { int iSum = 0; #pragma omp parallel for for (int i = 0; i < 100; ++i) { #pragma omp atomic iSum += i; } std::cout << iSum << std::endl; return 0; }
其中,在計算總合的部分,就需要透過 atomic 來避免不同執行序同時去修改 iSum 的值而造成錯誤。當然啦,理論上這邊用 reduction 會更合適就是了。
而這次的更新則是支援 OpenMP 3.1 的規範,加入對於 read、write、update、capture 的支援。
其中 update 會是預設值,如果像以前一樣什麼都不加、只寫 #pragma omp atomic 的話,基本上就是 #pragma omp atomic update。
而 read、write 就是會限定使用的情境,藉此來做到更好的最佳化;而、capture 感覺上則算是又擴展的支援的類型,而且也還支援有限度的程式區塊、而非單一的 expression statement。
下面就是 IBM 提供的表格:
Directive clause | expression_statement | structured_block |
---|---|---|
update | x++; |
|
read | v = x; |
|
write | x = expr; |
|
capture | v = x++; |
{v = x; x binop = expr;} |
其中,這邊的 expr 是指和 x 無關的純量型別(scalar type)的式子(expression);而 binop 是指 +、*、–、/、&、^、|、<<、>> 這些,OP 則是 ++ 或 --
。
更詳細的說明可以參考 OpenMP 官網的說明(連結)。
這篇大概就先這樣了。話說,微軟到底什麼時候才要把 libomp 的 dll 放到可轉發套件裡啊?