这段介绍的是 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)
cpp
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
:顺序执行策略,算法顺序执行,无切片。
使用示例:
cpp
std::vector<double> d(1000);
parallel::fill(par, begin(d), end(d), 0.0); // 使用默认的并行执行策略
重新绑定执行策略(Rebind Execution Policies)
- 你可以重新绑定执行器和执行参数,灵活控制并行任务的调度。
cpp
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); // 同时重新绑定执行器和参数
结合使用:
cpp
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<>
,允许异步控制流更方便地集成。
cpp
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
cpp
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
cpp
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)
cpp
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域中:
cpp#pragma omp parallel for schedule(static)
2. HPX实现方式
- 使用并行算法接口实现:
cpp
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感知数据与执行位置
- 创建执行目标 与执行器,并基于此定义分配器,确保数据放置到合适的内存域:
cpp
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); // 数据使用该分配器
- 结合执行策略设置细粒度控制:
cpp
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执行目标和执行器
cpp
cuda::target tgt("Tesla C2050"); // 指定具体GPU设备
using executor = cuda::default_executor;
using allocator = cuda::allocator<double>;
executor exec(tgt); // 创建执行器,指定执行位置(GPU)
allocator alloc(tgt); // 创建分配器,指定数据放置(GPU设备内存)
- 数据初始化和传输
cpp
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
执行策略,进行并行计算:cppinner_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
执行策略,启用向量化指令:cppinner_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节点上找最小最大元素:
cppstd::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上的例子类似:
cppstd::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)
-
普通循环:
cppparallel::for_loop( par, 0, a.size(), [&](int i) { a[i] = b[i] + 3.0 * c[i]; } );
-
带自动预取的循环:
cppparallel::for_loop( par.with(prefetch(b, c)), 0, a.size(), [&](int i) { a[i] = b[i] + 3.0 * c[i]; } );
-
意义:自动提前加载数据到缓存,减少内存访问延迟,提高性能。