Visual C++ OpenMP 更新:支援 3.1 的 atomic

| | 0 Comments| 10:48
Categories:

自從在 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 是用來要求系統在平行化的狀況下,一次只有一個執行序來執行特定的命令用的;相較於通用性的 criticalatomic 僅支援有限度的簡單運算,所以編譯器可以提供更好的最佳化、提供更好的效能(參考)。

下面就是一個簡單的例子:

#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 的規範,加入對於 readwriteupdatecapture 的支援。

其中 update 會是預設值,如果像以前一樣什麼都不加、只寫 #pragma omp atomic 的話,基本上就是 #pragma omp atomic update

readwrite 就是會限定使用的情境,藉此來做到更好的最佳化;而、capture 感覺上則算是又擴展的支援的類型,而且也還支援有限度的程式區塊、而非單一的 expression statement。

下面就是 IBM 提供的表格:

Directive clause expression_statement structured_block
update x++;
x--;
++x;
--x;
x binop = expr;
x = x binop expr;
x = expr binop x;
read v = x;
write x = expr;
capture v = x++;
v = x--;
v = ++x;
v = --x;
v = x binop = expr;
v = x = x binop expr;
v = x = expr binop x;
{v = x; x binop = expr;}
{v = x; xOP;}
{v = x; OPx;}
{x binop = expr; v = x;}
{xOP; v = x;}
{OPx; v = x;}
{v = x; x = x binop expr;}
{x = x binop expr; v = x;}
{v = x; x = expr binop x;}
{x = expr binop x; v = x;}
{v = x; x = expr;}

其中,這邊的 expr 是指和 x 無關的純量型別(scalar type)的式子(expression);而 binop 是指 +*/&^|<<>> 這些,OP 則是 ++--

更詳細的說明可以參考 OpenMP 官網的說明(連結)。


這篇大概就先這樣了。話說,微軟到底什麼時候才要把 libomp 的 dll 放到可轉發套件裡啊?

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *