透過 counting_iterator 讓 std::for_each_n 用索引值來操作


C++ 的標準函式庫雖然一直以來都有提供 <algorithm> 這個函式庫(參考),可以來做某些處理;而裡面其實也有像是 std::for_each()參考)或 std::for_each_n()參考)這類的函式、可以用來掃完整個陣列。

但是由於這些演算法大多是針對 iterator 來進行操作,在很多需要使用索引值(index)來存取周圍的資料時,會變得相對麻煩;也因此,其實 Heresy 這邊大多會使用傳統的 for 迴圈、或是 C++11 的 range-base for-loop 來寫,而很少會去用 std::for_each()

也因此,當 C++17 開始正式針對部分也算法提供平行化的能力的時候(參考),雖然高興了一陣子,但是卻也發現有點難以使用…

不過,之前在看到 NVIDIA 的《Accelerating Standard C++ with GPUs Using stdpar》這篇文章,發現它號稱 NVIDIA HPC SDK 的 NVC++ 可以將 C++17 的平行化演算法編譯成可以在 NVIDIA GPU 上執行的程式時,倒是覺得好像又有點用了?

而實際上,在這篇文章中,NVIDIA 也有提供簡單的範例,示範怎麼把 OpenMP 改寫成 std::for_each_n() 的形式;他這邊給的例子,本來的寫法是:

#pragma omp parallel for firstprivate(qlc_monoq)
for (Index_t i = 0; i < domain.regElemSize(r); ++i) {
}

改成用 std::for_each_n() 後,變成:

std::for_each_n(std::execution::par,
  counting_iterator(0), domain.regElemSize(r),
  [=, &domain](Index_t i) {
  }
);

可以看到,他這邊是透過 counting_iterator 來做為操作用的 iterator、並計算索引值;看起來還算滿方便的?

不過,counting_iterator 並不是 C++ STL 的一部分,這邊 NVIDIA 用的,應該是 NVIDIA 的 Thrust 這個函式庫(官方文件)裡的型別(文件)。

而如果想要使用的話,在 Boost C++ Libraries 的 Iterator 函式庫(文件)裡面,也有提供同樣功能的 counting_iterator文件);所以透過 Boost 提供的 counting_iterator,也就可以讓 std:for_each() 的寫法更接近傳統的 for-loop 了。

下面就是一個最簡單的範例:

#include <algorithm>
#include <boost/iterator/counting_iterator.hpp>
 
int main()
{
  int aData[100];
 
  // for-loop
  for (int idx = 0; idx < 100; ++idx)
  {
    aData[idx] = idx;
  }
 
  // std::for_each_n
  std::for_each_n(boost::counting_iterator(0), 100, [&aData](const auto& idx) {
    aData[idx] = idx;
  });
}

在上面的例子裡面,兩個迴圈的寫法是同樣意義的。

如果是要用 std:for_each() 的話,則可以寫成:

std::for_each(boost::counting_iterator(0), boost::counting_iterator(100),
  [&aData](const auto& idx) {
    aData[idx] = idx;
  }
);

而由於這邊還是透過索引值來操作,所以如果要用來計算、並存取周圍的資料,也就可以用一樣的方法來寫了~尤其在影像處理的時候,這樣的寫法會是相對方便很多的!


而如果要平行化的話,使用 OpenMP 的話,只要加上一行就可以了:

// for-loop with OpenMP
#pragma omp parallel for
for (int idx = 0; idx < 100; ++idx)
{
  aData[idx] = idx;
}

而如果是使用 std:for_each_n() 的話呢,如果是 C++17 的話,則只要加上 execution policy 就可以了!

#include <algorithm>
#include <execution>
#include <boost/iterator/counting_iterator.hpp>
 
int main()
{
  int aData[100];
 
  // std::for_each_n
  std::for_each_n(std::execution::par,
    boost::counting_iterator(0), 100, [&aData](const auto& idx) {
    aData[idx] = idx;
  });
}

這邊只要先 include <execution> 這個 header 檔後,只要在呼叫 std:for_each_n() 的時候,透過第一個引數指定要怎麼執行就可以了(文件)。

這樣的寫法和 OpenMP 相比,其實也是很簡單的~

而在 execution policy 的部分,到 C++20 時總共有四種:

  • sequenced_policy
  • parallel_policy
  • parallel_unsequenced_policy
  • unsequenced_policy (C++20)

其中,parallel 就是平行化、而 unsequenced 則是進行向量化(參考)。

理論上,不同的編譯器實作,也可以提供更多的選項;像是 SYCL 也有提供 Parallel STL 的實作(參考),理論上可以適用於 OpenCL 所支援的裝置。

以個人來說,還是比較期待以後 Visual C++ 和 g++ 可以直接支援 GPU 加速的實作啊~

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.