C++17 與 C++20 針對演算法的平行化

| | 0 Comments| 08:39|
Categories:

「Parallel algorithms and execution policies」是 C++17 加入標準函式庫的一個演算法功能,他基本上讓開發者可以在呼叫 <algorithm> 提供的演算法的時候、只要加入一個新的參數、就可以完成程式的平行化,算是相當實用的功能。

例如要透過 std::sort() 來把一個 std::vector<> 的資料 vData 進行排序的話,可以寫成:

std::sort(vData.begin(), vData.end());

而在 C++17 後,如果覺得 vData 的資料太多、排序要花的時間太久的話,則可以改成:

std::sort(std::execution::par, vData.begin(), vData.end());

這邊只要多加入一個 std::execution::par(需要 include <execution>)、告訴編譯器要平行化執行這個演算法就可以了!相當容易使用!

完整的程式碼大概是下面的樣子:

#include <iostream>
#include <vector>
 
#include <algorithm>
#include <execution>
 
int main()
{
  std::vector vData = { 1,3,5,7,2,4,6 };
  std::sort(vData.begin(), vData.end());
 
  // after C++17
  std::sort(std::execution::par, vData.begin(), vData.end());
 
  for (const auto& v : vData)
    std::cout << v << ' ';
}

標準函式庫這邊的設計、是只要在 <algorithm> 提供的演算法的第一個參數、多加一個 execution policy(執行策略、Cpp Reference)就可以要求函式庫應該要用什麼方法來執行。

目前總共提供四種 execution policy:

  • std::execution::sequenced_policy:依序執行、不要平行化
  • std::execution::parallel_policy:平行化處理
  • std::execution::parallel_unsequenced_policy:平行化並向量化
  • std::execution::unsequenced_policy:向量化

而實際在使用的時候,則是要使用對應的物件(文件):

  • std::execution::seq
  • std::execution::par
  • std::execution::par_unseq
  • std::execution::unseq

其中,前三者都是在 C++17 就有了,而第四項則是在 C++20 才加入的。

不過要注意的是,雖然可以指定 execution policy 要求函式庫使用什麼方法來做運算、但是實際上最後會不會真的照指定的方法去做、其實還是要看使用的函示庫是怎麼實作的。


簡單的效能測試

下面是一個 C++ Reference 給的例子(稍微簡化一點):

#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <vector>
 
#include <execution>
 
void measure(auto policy, std::vector<int> v)
{
  const auto start = std::chrono::steady_clock::now();
  std::sort(policy, v.begin(), v.end());
  const auto finish = std::chrono::steady_clock::now();
  std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(finish - start).count() << '\n';
};
 
int main()
{
  // generate a data array with random generator
  std::vector<int> v(10'000'000);
  std::mt19937 gen{ std::random_device{}() };
  std::ranges::generate(v, gen);
 
  // sort in different policy
  measure(std::execution::seq, v);
  measure(std::execution::unseq, v);
  measure(std::execution::par, v);
  measure(std::execution::par_unseq, v);
}

這邊是先透過 C++ 20 RangesC++11 的亂數產生器來產生一個數量很多的亂數資料。

之後,則是透過 measure() 這個函式,來使用上面四種 execution policy 執行 std:sort() 來對資料進行排序、並計算所需要的時間。

在 Heresy 這邊在 Visual Studio 2022 上測試的結果是:

565
563
71
75

基本上,在這個例子來說,有沒有向量化(unsequenced) 沒有明顯的差異、甚至可能還慢一點;但是有沒有平行化(parallel)則是有相當大的差距!速度快了超過 7 倍~

所以如果真的有需要的話,透過這樣簡單的修改就可以加速、其實是很方便的。


在 g++ 使用

如果是要使用 g++ 的話,有一點可能要稍微注意一下。

在只有安裝 g++(這邊是測試 g++ 12)的狀況下、直接用「g++ --std=c++20 main.cpp」編譯的話,雖然也可以成功編譯,但是執行的時候應該會發現所需要的時間並沒有顯著地減少、感覺不出來有平行化。

這應該是因為 g++ 本身預設提供的 STL 雖然有提供對應的函式介面、但是實際的實作應該是沒有真的實作平行化的演算法。所以如果有需要真的支援平行化的,要另外安裝「libtbb-dev(Threading Building Blocks)、然後連結的時候要加上「-ltbb才行。

以簡單的程式來說,編譯的指令會變成「g++ --std=c++20 main.cpp -ltbb」;而同一個範例的結果會是類似下面的狀況(-O2):

446
463
39
30

感覺上 g++(或者說 TBB)的平行化的效益似乎是比 MSVC 來的好?速度快了 12 倍以上。


基本上,個人覺得 C++ 加入演算法平行化功能最大的好處,應該還是有機會可以直接使用編譯器提供的各項平行化演算法。

不過老實說,個人以前真的有拿來用的…好像比較多的機會只有 sort 而已? XD
其他會用到的,好像頂多就是 all_of()max_element()count_if() 這類相對簡單的?而實際上,有的時候甚至也會因為資料性質的關係、自己用 for 迴圈去寫了。

而且這邊比較尷尬的,是之前就發現在目前 MSVC 提供的 STL 裡面,max_element() 這幾個函式實際上根本沒有實作平行化…裡面註解直接寫著:

not parallelized at present, parallelism expected to be feasible in a future 
release

所以如果真的想要比較快,可能還得看微軟什麼時候補上了。

至於 transform()for_each(),雖然也是可以用來處理陣列裡面的資料,但是便利性上總覺得不算很高?尤其是要使用 index 來存取不同的資料的時候,直接用 for 迴圈加上 OpenMP 感覺還是比較好的選擇。(額外參考:《透過 counting_iterator 讓 std::for_each_n 用索引值來操作》)

不過另一方面,要使用 OpenMP 的缺點就是需要編譯器在語法上支援,而 Visual Studio 在 OpenMP 的支援真的還是滿糟的就是了…到現在標準編譯選項還是只有 OpenMP 2.0 而已…(光是 for 迴圈只能用無號整數的限制就很頭大了)

雖然有在透過新的 LLVM OpenMP 來支援比較新的標準,但是不但支援性還是不完整,而且到現在也都屬於測試階段、需要的 DLL 到現在也都還在 nonredist 的資料夾下、沒有放在可轉發套件裡面…感覺現階段其實還不適合拿來實用。

最後,以標準演算法的平行化來說,個人其實還是希望以後 STL 看看能不能加入新的 execution policy、支援 GPGPU 或其他加速器。

又或者,由於這部分只需要函式庫層級的支援、所以如果有第三方有實作的話,或許也是有機會可以拿來當擴展使用的。像是 AdaptiveCpp(GitHub)其實應該就是有提供將標準函示酷的演算法用 GPU 來計算的功能(參考);只不過要在 Visual Studio 使用感覺還很麻煩、而且有些問題就是了(參考)。


補充:實際上 Heresy 在很久以前,就曾經有寫過一篇《Visual C++ 2013 的 STL 平行化函示庫》,介紹了當時還不是 C++ 標準的 Parallel STL;而在 C++17 納入標準的時候,大致上就是 namespace 之類有做了一點修改。

而現在基本上各家編譯器應該也都有支援了(但是內部實作完不完整就再說了),所以這邊算是拿標準的版本來補一下紀錄了。

Leave a Reply

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