CppCon 2016 学习:Parallelism in Modern C++

这段介绍的是 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::threadstd::mutex
    • 异步任务和未来(std::futurestd::async
    • 函数适配器和函数对象(std::bindstd::function
    • 容器和辅助类型(std::tuplestd::anystd::vector
    • 并行算法(std::parallel::for_eachstd::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_eachhpx::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_policydataseq_execution_policy 是新的执行策略,支持自动向量化代码。
  • 他们还有异步版本:datapar_task_execution_policydataseq_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 :默认执行器,对应 seqpar 执行策略。
  • 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提供了两种主要的数据容器:
    1. hpx::vector<T, Alloc>
      • 接口与 std::vector<T>一致。
      • 通过自定义分配器(allocator)管理数据局部性。
      • 可以指定数据放置目标(NUMA域、GPU、远程节点等)。
    2. hpx::partitioned_vector
      • 同样接口与 std::vector<T>一致。
      • 底层是分段存储(segmented data store)。
      • 每个段可以是 hpx::vector<T, Alloc>
      • 使用分布策略(distribution_policy)控制数据放置和访问。
      • 支持操作跨多个执行目标的数据。

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
  • 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 支持基于标准算法轻松扩展,并且直接支持异步执行。
  • 结合 futuredataflow 和协程 (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
  • 最优性能依赖于数据正确放置,即数据需要在执行线程所在的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::copyparallel::transform等)无缝调度代码在CPU或GPU。
  • 数据放置由GPU专用分配器管理,保证数据实际存放在GPU内存。
  • 执行器保证计算任务实际在GPU上运行,最大限度减少数据传输和延迟。

这部分内容主要讲了C++中并行与向量化执行的结合,以及数据的分区和优化执行手段。我帮你总结重点:

1. 向量化示例:点积 (Dot-product)

  • 普通并行执行
    使用 par 执行策略,进行并行计算:

    cpp 复制代码
    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 执行策略,启用向量化指令:

    cpp 复制代码
    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节点上找最小最大元素:

    cpp 复制代码
    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上的例子类似:

    cpp 复制代码
    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)

  • 普通循环:

    cpp 复制代码
    parallel::for_loop(
      par, 0, a.size(),
      [&](int i) { a[i] = b[i] + 3.0 * c[i]; }
    );
  • 带自动预取的循环:

    cpp 复制代码
    parallel::for_loop(
      par.with(prefetch(b, c)), 0, a.size(),
      [&](int i) { a[i] = b[i] + 3.0 * c[i]; }
    );
  • 意义:自动提前加载数据到缓存,减少内存访问延迟,提高性能。

相关推荐
im_AMBER1 小时前
java复习 19
java·开发语言
小猫咪怎么会有坏心思呢1 小时前
华为OD机考-异常的打卡记录-字符串(JAVA 2025B卷)
java·开发语言·华为od
梦境虽美,却不长2 小时前
C++ 学习 多线程 2025年6月17日18:41:30
c++·学习·线程·异步
泓博2 小时前
KMP(Kotlin Multiplatform)简单动画
android·开发语言·kotlin
一只理智毅2 小时前
copy-and-swap语义
c++
芒果快进我嘴里2 小时前
C++打印乘法口诀表
开发语言·c++
2 小时前
Lua基础复习之Lua元表
开发语言·lua
可能是猫猫人2 小时前
【Python打卡Day39】图像数据与显存 @浙大疏锦行
开发语言·python
爬虫程序猿2 小时前
利用 Python 爬虫获取 Amazon 商品详情:实战指南
开发语言·爬虫·python
_w_z_j_2 小时前
C++----剖析stack、queue
开发语言·c++