這段介紹的是 HPX (High Performance ParalleX),一個現代C++的通用并行運行時系統,重點包括:
- 通用性:適用于各種規模的應用,從小型到超大規模分布式系統。
- 統一標準API:符合C++標準,方便編寫異步、并行、分布式和異構計算程序。
- 極大并發支持:能管理數億個輕量級線程。
- 一致的局部與遠程調用語義:無論本地還是遠端操作,API調用保持一致。
- 技術核心:
- AGAS(Active Global Address Space)提供全局系統地址空間。
- 細粒度并行和輕量同步機制。
- 基于工作隊列的消息驅動計算。
- 顯式支持硬件加速和向量化。
HPX對比傳統的OpenMP/MPI框架,在性能和可擴展性方面表現更優。
它開源,基于Boost許可,社區活躍,可用于科研和實驗平臺。
結構圖中展示了HPX的各個組成模塊,從C++1y并行API到底層操作系統接口,包括網絡通信層(Parcel Transport Layer)和全局地址空間(AGAS)。
如果你需要,我可以幫你:
- 展開講解HPX的具體使用方式和編程模型
- 舉例演示簡單的HPX并行程序
- 比較HPX與傳統MPI/OpenMP的區別
- 說明它在異步和分布式編程上的優勢
這部分講的是 HPX 的 API 設計原則和組成,強調了 HPX 在接口設計上:
- 盡量貼近 C++1y 標準庫,包括但不限于:
- 線程和同步機制(
std::thread
、std::mutex
) - 異步任務和未來(
std::future
、std::async
) - 函數適配器和函數對象(
std::bind
、std::function
) - 容器和輔助類型(
std::tuple
、std::any
、std::vector
) - 并行算法(
std::parallel::for_each
、std::parallel::task_block
) - 標準輸出流(
std::cout
)
- 線程和同步機制(
- HPX 對應的實現幾乎一一對應:
hpx::thread
hpx::mutex
hpx::future
hpx::async
hpx::bind
hpx::function
hpx::tuple
hpx::any
hpx::cout
- 并行算法也有對應的擴展:
hpx::parallel::for_each
,hpx::parallel::task_block
- 容器如
hpx::vector
和分布式的hpx::partitioned_vector
- 擴展標準接口,但保持和標準庫的兼容性,這樣學習和遷移成本低。
總結:HPX 是對 C++ 標準庫的“平行與分布式增強版”,你可以用非常熟悉的接口寫并行、異步、分布式程序,同時獲得更強的可擴展性和性能。
這部分內容講的是現代C++中并行編程的現狀、概念以及未來愿景,重點如下:
1. 并行編程的種類與概念
- 并行算法(Parallel Algorithms):如并行的
for_each
等,針對數據迭代的并行。 - 異步任務(Asynchronous):
futures
,async
,dataflow
等,異步執行和任務依賴。 - 分叉-合并模型(Fork-Join):任務拆分成多個并行任務后合并結果。
- 執行器(Executors):管理任務的執行環境和策略,如線程池、線程調度。
- 執行策略(Execution Policies):決定任務執行的策略,比如順序執行、并行執行、矢量化執行。
- 顆粒度(Grainsize):控制任務劃分的細粒度,影響性能和負載均衡。
2. C++當前并行標準狀態
- Parallelism TS:提供了數據并行算法,已經并入C++17標準。
- Concurrency TS:定義了基于任務的異步和繼續(continuation)式并行。
- 任務塊(task blocks,N4411):支持異構任務的分叉-合并并行。
- 執行器提案(executors,N4406):管理和調度任務執行。
- 協程支持(resumable functions,co_await):異步函數等待機制。
3. 目前仍缺失的特性
- 上述各種并行特性的更好整合與統一。
- 并行范圍(parallel ranges),即直接支持范圍算法的并行。
- 矢量化支持(SIMD)正在討論中。
- 針對GPU、多核、多節點分布式的擴展支持。
4. 未來愿景
- 目標是使C++內建并行能力不依賴外部技術(如OpenMP、OpenACC、CUDA等)。
- HPX嘗試讓C++不再依賴MPI,實現統一的分布式異步并行運行時。
總結:
這段話體現了C++對并行性的標準化趨勢,未來C++會有一個完整、統一、跨平臺、跨硬件的并行API體系,讓程序員不用直接面對各種外部并行框架,而HPX是推動這一目標的實踐之一。
這部分介紹了**Future(未來對象)**的概念及其作用,重點如下:
什么是 Future?
- Future 是一個對象,表示某個尚未計算完成的結果。
- 它代表一個異步操作的最終結果,但在調用時結果可能還沒準備好。
- 通過 Future,可以讓程序的執行線程暫時掛起(Suspend),等待另一個線程完成計算后再恢復(Resume)。
- 這個機制可以在不同的“本地性(Locality)”之間協作,比如不同線程,甚至分布式節點。
Future 的優勢
- 透明同步:程序員不用顯式管理線程同步,Future 會自動等待結果準備好。
- 隱藏線程細節:不用直接操作線程,只要獲取結果即可。
- 可管理的異步性:讓異步編程變得更容易。
- 支持組合多個異步操作:可以鏈式組合多個 Future,實現復雜的異步工作流。
- 把并發(concurrency)轉化為并行(parallelism):即實現真正的并行計算,而非僅僅是切換任務。
Future 的示例(用 C++ 標準庫 async)
int universal_answer() { return 42; }
void deep_thought()
{std::future<int> promised_answer = std::async(&universal_answer);// 這里可以做別的事情std::cout << promised_answer.get() << std::endl; // 打印 42
}
std::async
啟動一個異步任務,返回一個 future 對象。promised_answer.get()
阻塞直到結果準備好,然后返回結果。
總結:
Future 是現代C++異步編程的核心機制,簡化了多線程編程的復雜度,讓異步任務的結果能以同步的方式獲取,從而更容易寫出安全、可維護的并行代碼。
這部分講的是現代C++中的并行算法(Parallel Algorithms),特別是基于執行策略(Execution Policies)的設計,以及 HPX 對執行策略的擴展。
并行算法的核心思想
- 現代C++標準庫(C++17及以后)引入了執行策略,作為算法的第一個參數,決定算法是順序執行還是并行執行。
- 執行策略是一個策略對象,它關聯了**執行器(executor)**和相關的執行參數(executor parameters)。
- 常見執行策略:
par
:并行執行策略,默認使用并行執行器,通常帶有靜態切片(chunk)大小。seq
:順序執行策略,算法順序執行,無切片。
使用示例:
std::vector<double> d(1000);
parallel::fill(par, begin(d), end(d), 0.0); // 使用默認的并行執行策略
重新綁定執行策略(Rebind Execution Policies)
- 你可以重新綁定執行器和執行參數,靈活控制并行任務的調度。
numa_executor exec; // 自定義執行器(例如基于NUMA的執行器)
static_chunk_size param; // 自定義執行參數(chunk大小)
auto policy1 = par.on(exec); // 只重新綁定執行器
auto policy2 = par.with(param); // 只重新綁定執行參數
auto policy3 = par.on(exec).with(param); // 同時重新綁定執行器和參數
結合使用:
my_executor my_exec = ...;
my_params my_par = ...;
parallel::fill(par.on(my_exec).with(my_par), begin(d), end(d), 0.0);
- 這樣,算法會用你自定義的執行器和參數來執行。
HPX 對執行策略的擴展
- HPX 增加了異步執行策略,如:
parallel_task_execution_policy
— 異步版本的parallel_execution_policy
sequential_task_execution_policy
— 異步版本的sequential_execution_policy
- 使用異步執行策略,原本同步的算法調用將返回
future<>
,允許異步控制流更方便地集成。
auto fut = parallel::fill(par(task), begin(d), end(d), 0.0);
// fut 是一個 future,表示填充操作異步完成
總結
- 執行策略是現代C++并行算法的核心,決定算法的執行方式。
- 通過綁定不同執行器和參數,可以靈活調整任務調度和執行細節。
- HPX 在此基礎上支持異步版本執行策略,進一步增強異步并行控制能力。
如果你想,我可以幫你寫示例代碼或者解釋執行器和chunk大小的具體作用。你需要嗎?
NUMA 是 Non-Uniform Memory Access(非統一內存訪問)的縮寫。
什么是 NUMA?
NUMA 是一種計算機內存架構設計,常見于多處理器系統(尤其是多核、多插槽服務器):
- 內存訪問速度因訪問位置不同而不同,訪問本地(靠近處理器的)內存比訪問遠端內存快。
- 系統被劃分成若干個“節點”(node),每個節點包含一個或多個CPU核心和一部分本地內存。
- 當一個處理器訪問它自己節點的內存時,速度快;訪問其他節點內存時,速度慢。
為什么有 NUMA?
傳統的 Uniform Memory Access (UMA) 架構中,所有處理器訪問內存的速度是一樣的。
但是隨著處理器數量的增加,單一共享內存總線的帶寬限制和延遲成為瓶頸。
NUMA 通過把內存分割到多個節點,讓每個節點有自己的本地內存,減少總線爭用,提高擴展性和性能。
NUMA對程序的影響?
- 需要考慮數據和計算的“親和性”(affinity),即讓處理器訪問本地內存。
- 程序若忽略NUMA,可能因為頻繁訪問遠端內存而性能下降。
- 并行程序可以用 NUMA-aware(感知NUMA)的執行器來優化調度和內存訪問,提升性能。
簡單總結
方面 | 解釋 |
---|---|
NUMA | 非統一內存訪問架構 |
設計目標 | 解決多核多處理器內存帶寬瓶頸 |
特點 | 不同內存訪問延遲不同 |
優化方式 | 讓線程和數據“親近”,減少遠端訪問 |
這部分內容擴展了HPX對執行策略和執行器的設計,重點包括矢量化執行策略(vectorization execution policies)、執行器(executors)和執行參數(executor parameters)。
矢量化執行策略(HPX Extensions)
- datapar_execution_policy 和 dataseq_execution_policy 是新的執行策略,支持自動向量化代碼。
- 他們還有異步版本:
datapar_task_execution_policy
和dataseq_task_execution_policy
,分別通過datapar(task)
和dataseq(task)
生成。 - 作用:指示算法對數據類型進行特定轉換,啟用SIMD向量指令以加速運算。
- 依賴庫:
- Vc (向量化庫)
- 可能還有 Boost.SIMD
- 需要用到C++14的泛型lambda或多態函數對象。
執行器(Executors)
- 執行器必須實現一個函數
async_execute(F&& f)
,用來異步執行任務f
。 - 通過
executor_traits
統一調用接口,支持:- 單個任務異步執行
- 單個任務同步執行
- 批量任務異步執行
- 批量任務同步執行
- 異步調用會返回
future
。
執行器示例
- sequential_executor, parallel_executor:默認執行器,對應
seq
和par
執行策略。 - this_thread_executor:任務在當前線程執行。
- distribution_policy_executor:分布式執行器,按分布式策略選擇節點。
- host::parallel_executor:指定核或NUMA節點執行,支持NUMA感知。
- cuda::default_executor:用GPU執行任務。
執行參數(Executor Parameters)
- 與執行器類似,參數通過
executor_parameter_traits
管理。 - 功能示例:
- 控制粒度(grain size),即單線程處理多少迭代。
- 類似OpenMP的調度策略:靜態、動態、引導(guided)等。
- 支持自動、靜態、動態chunk大小。
- GPU專用參數,如指定GPU核函數名
gpu_kernel<foobar>
。 - 預取相關數組。
總結
- HPX 提供了豐富的執行策略擴展,支持同步/異步及矢量化執行。
- 執行器和執行參數接口設計靈活,便于自定義和擴展。
- 這種設計使得C++程序能夠細粒度地控制并行任務調度、硬件親和性和性能優化。
這部分講的是數據放置(Data Placement),重點是如何在多樣化的硬件平臺(NUMA架構、GPU、分布式系統)上高效地管理和訪問數據,確保并行計算的性能和效率。
數據放置的挑戰與需求
- 不同平臺有不同的數據放置策略,比如:
- NUMA(非統一內存訪問)架構:數據應當靠近使用它的處理器節點,避免跨節點訪問帶來的延遲。
- GPU:數據需要顯式從CPU內存轉移到GPU內存。
- 分布式系統:數據分散在多臺機器,需要網絡通信(RDMA等)來訪問。
- 需要一個統一接口方便控制數據的顯式放置。
標準接口和HPX的支持
- 使用
std::allocator<T>
接口擴展:- 支持批量操作(分配、構造、銷毀、釋放)。
- 支持控制數據放置。
- HPX提供了兩種主要的數據容器:
- hpx::vector<T, Alloc>
- 接口與
std::vector<T>
一致。 - 通過自定義分配器(allocator)管理數據局部性。
- 可以指定數據放置目標(NUMA域、GPU、遠程節點等)。
- 接口與
- hpx::partitioned_vector
- 同樣接口與
std::vector<T>
一致。 - 底層是分段存儲(segmented data store)。
- 每個段可以是
hpx::vector<T, Alloc>
。 - 使用分布策略(distribution_policy)控制數據放置和訪問。
- 支持操作跨多個執行目標的數據。
- 同樣接口與
- hpx::vector<T, Alloc>
allocator_traits 擴展
- 新增數據復制功能:
- CPU平臺上直接復制。
- GPU上需要特定平臺的數據傳輸(與
parallel::copy
結合)。 - 分布式環境下通過網絡(如RDMA)復制數據。
- 訪問單個元素的功能:
- CPU上簡單直接。
- GPU上訪問較慢,但可以實現。
- 分布式環境中通過網絡實現。
總結
- 數據放置是性能關鍵點,尤其在異構和分布式系統中。
- HPX通過擴展標準的allocator機制,實現對數據位置的靈活管理。
- 這樣設計使得程序員可以透明、高效地操作復雜硬件上的數據。
這部分講的是 Execution Targets(執行目標) 的概念,是數據放置和任務執行的核心抽象。
關鍵點總結:
- Execution Targets 是系統中的“地點”(opaque types,不透明類型),代表數據或計算的執行位置。
- 它們用于:
- 標識數據的放置位置,確保數據在哪個硬件或節點上。
- 指定執行代碼的地點,使得計算在靠近數據的地方運行,降低數據傳輸延遲,提高性能。
- Execution Targets 封裝了底層架構的細節,比如:
cuda::target
—— 表示GPU設備。host::target
—— 表示CPU或NUMA節點。
- Allocator(內存分配器)可以基于 Execution Targets 初始化,例如:
- NUMA域上的分配器
host::block_allocator
- GPU設備上的分配器
cuda::allocator
- NUMA域上的分配器
- Executors(執行器)同樣可以基于 Execution Targets 初始化,確保代碼執行在目標硬件附近。
總結
Execution Targets 就像“指向”系統中特定硬件資源的句柄,幫助程序員控制數據放置和代碼運行的物理位置,從而優化并行程序的性能。
這部分展示了如何擴展并行算法,以同步和異步兩種方式實現新算法 gather
,通過 HPX 的并行算法和異步機制使代碼更靈活高效。
核心點總結:
1. 同步版本 gather
template <typename BiIter, typename Pred>
pair<BiIter, BiIter> gather(BiIter f, BiIter l, BiIter p, Pred pred)
{BiIter it1 = stable_partition(f, p, not1(pred));BiIter it2 = stable_partition(p, l, pred);return make_pair(it1, it2);
}
- 基于標準算法
stable_partition
實現的同步版本。 - 先對區間
[f, p)
做一次穩定分區,再對[p, l)
做一次分區。 - 返回兩個迭代器表示分區邊界。
2. 異步版本 gather_async
template <typename BiIter, typename Pred>
future<pair<BiIter, BiIter>> gather_async(BiIter f, BiIter l, BiIter p, Pred pred)
{future<BiIter> f1 = parallel::stable_partition(par(task), f, p, not1(pred));future<BiIter> f2 = parallel::stable_partition(par(task), p, l, pred);return dataflow(unwrapped([](BiIter r1, BiIter r2) { return make_pair(r1, r2); }),f1, f2);
}
- 使用
parallel::stable_partition
帶有異步執行策略par(task)
,返回future<BiIter>
。 - 用
dataflow
來組合兩個future
,結果也是一個future<pair<...>>
,等待兩個異步任務完成后合成結果。
3. 異步版本(使用 co_await)
template <typename BiIter, typename Pred>
future<pair<BiIter, BiIter>> gather_async(BiIter f, BiIter l, BiIter p, Pred pred)
{future<BiIter> f1 = parallel::stable_partition(par(task), f, p, not1(pred));future<BiIter> f2 = parallel::stable_partition(par(task), p, l, pred);return make_pair(co_await f1, co_await f2);
}
- 這是利用 C++20 的協程(
co_await
)語法寫的版本。 - 代碼更簡潔,表達了等待兩個異步結果,然后組合返回。
總結
- HPX 支持基于標準算法輕松擴展,并且直接支持異步執行。
- 結合
future
、dataflow
和協程 (co_await
) 讓異步編程更自然且高效。 - 你可以根據場景選擇同步或異步版本,充分利用現代C++并行和異步特性。
這部分介紹了STREAM基準測試,它是衡量內存帶寬性能的經典測試,重點是測量內存數據訪問的效率,尤其在多核NUMA架構下,數據的放置位置對性能有巨大影響。
重點總結:
1. STREAM基準測試簡介
- 測試三個數組
a
,b
,c
的操作:- copy:
c = a
- scale:
b = k * c
- add:
c = a + b
- triad:
a = b + k * c
- copy:
- 最優性能依賴于數據正確放置,即數據需要在執行線程所在的NUMA內存域。
- OpenMP中通常用“first touch”原則保證數據在正確的NUMA域中:
#pragma omp parallel for schedule(static)
2. HPX實現方式
- 使用并行算法接口實現:
std::vector<double> a, b, c; // 數據
// 初始化數據...
auto a_begin = a.begin(), a_end = a.end(), b_begin = b.begin(), b_end = b.end(), c_begin = c.begin(), c_end = c.end();
// copy step: c = a
parallel::copy(par, a_begin, a_end, c_begin);
// scale step: b = k * c
parallel::transform(par, c_begin, c_end, b_begin,[](double val) { return 3.0 * val; });
// add step: c = a + b
parallel::transform(par, a_begin, a_end, b_begin, b_end, c_begin,[](double val1, double val2) { return val1 + val2; });
// triad step: a = b + k * c
parallel::transform(par, b_begin, b_end, c_begin, c_end, a_begin,[](double val1, double val2) { return val1 + 3.0 * val2; });
- 這些算法都采用執行策略
par
表明并行執行。
3. NUMA感知數據與執行位置
- 創建執行目標與執行器,并基于此定義分配器,確保數據放置到合適的內存域:
host::target tgt("numa=0"); // 綁定NUMA節點0
using executor = host::parallel_executor;
using allocator = host::block_allocator<double>;
executor exec(tgt); // 執行器指定運行地點
allocator alloc(tgt, ...); // 分配器指定數據放置
vector<double, allocator> a(alloc), b(alloc), c(alloc); // 數據使用該分配器
- 結合執行策略設置細粒度控制:
auto policy = par.on(exec).with(static_chunk_size());
parallel::copy(policy, a_begin, a_end, c_begin);
// ...
4. HPX vs OpenMP
- OpenMP通過“first touch”隱式保證數據放置,HPX則顯式控制執行位置和數據分配,更加靈活且適合復雜NUMA、多節點、加速器環境。
這部分講的是如何用HPX擴展STREAM基準測試到GPU上執行,利用HPX的異構執行模型。
重點總結:
- 定義GPU執行目標和執行器
cuda::target tgt("Tesla C2050"); // 指定具體GPU設備
using executor = cuda::default_executor;
using allocator = cuda::allocator<double>;
executor exec(tgt); // 創建執行器,指定執行位置(GPU)
allocator alloc(tgt); // 創建分配器,指定數據放置(GPU設備內存)
- 數據初始化和傳輸
std::vector<double> data = { ... }; // 初始化數據在主機內存(CPU)
hpx::vector<double, allocator> a(alloc), b(alloc), c(alloc); // 設備上的數據容器
parallel::copy(par, data.begin(), data.end(), a.begin()); // 將數據從主機復制到GPU設備內存
- 在GPU上執行STREAM基準測試
通過指定執行策略結合GPU執行器執行并行算法,實現基于GPU的并行計算。
說明
- HPX讓你用統一的并行算法接口(如
parallel::copy
、parallel::transform
等)無縫調度代碼在CPU或GPU。 - 數據放置由GPU專用分配器管理,保證數據實際存放在GPU內存。
- 執行器保證計算任務實際在GPU上運行,最大限度減少數據傳輸和延遲。
這部分內容主要講了C++中并行與向量化執行的結合,以及數據的分區和優化執行手段。我幫你總結重點:
1. 向量化示例:點積 (Dot-product)
- 普通并行執行
使用par
執行策略,進行并行計算:inner_product(par, // 并行執行策略std::begin(data1), std::end(data1),std::begin(data2),0.0f,[](auto t1, auto t2) { return t1 + t2; }, // 累加[](auto t1, auto t2) { return t1 * t2; } // 乘法 );
- 并行 + 向量化執行
使用datapar
執行策略,啟用向量化指令:inner_product(datapar, // 并行且向量化執行策略std::begin(data1), std::end(data1),std::begin(data2),0.0f,[](auto t1, auto t2) { return t1 + t2; },[](auto t1, auto t2) { return t1 * t2; } );
- 效果
多核CPU上,datapar
策略利用SIMD指令,獲得比單純并行更高的速度提升。
2. 分區向量(Partitioned Vector)
- 支持數據分布在多個執行目標(NUMA節點、GPU等)。
- 例子:在NUMA節點上找最小最大元素:
std::vector<targets> targets = host::get_numa_targets(); partitioned_vector<int> v(size, host::target_distribution_policy(targets)); host::numa_executor exec(targets); generate(par.on(exec), v.begin(), v.end(), rand); auto iters = minmax_element(par.on(exec), v.begin(), v.end());
- GPU上的例子類似:
std::vector<targets> targets = cuda::get_device_targets(); partitioned_vector<int> v(size, cuda::target_distribution_policy(targets)); cuda::default_executor exec(targets); generate(par.on(exec), v.begin(), v.end(), rand); auto iters = minmax_element(par.on(exec), v.begin(), v.end());
3. 自動循環預取 (Loop Prefetching)
- 普通循環:
parallel::for_loop(par, 0, a.size(),[&](int i) { a[i] = b[i] + 3.0 * c[i]; } );
- 帶自動預取的循環:
parallel::for_loop(par.with(prefetch(b, c)), 0, a.size(),[&](int i) { a[i] = b[i] + 3.0 * c[i]; } );
- 意義:自動提前加載數據到緩存,減少內存訪問延遲,提高性能。