mold 仓库中 oneTBB parallel_reduce 并行算法示例详解:pi、primes 与 convex_hull 实战剖析
mold 仓库中 oneTBB parallel_reduce 并行算法示例详解pi、primes 与 convex_hull 实战剖析【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读oneAPI Threading Building BlocksoneTBB是 mold 链接器仓库所携带的第三方高性能运行时库而parallel_reduce是其最核心的并行归约算法之一。本文以本仓库 third-party/tbb/examples/parallel_reduce/README.md 为骨架系统讲解其中三个官方示例——convex_hull凸包、pi数值积分求 π、primes埃拉托斯特尼素数筛的构建方式、运行参数与命令行接口并结合仓库内的 pi.cpp、primes.cpp、convex_hull.hpp 等源码深入剖析parallel_reduce的 Body 协议operator()、join()、分裂构造函数、自定义 Range 与粒度控制、simple_partitioner以及懒分裂lazy splitting等底层机制。读完本文你将掌握 oneTBBparallel_reduce从入门到进阶的完整实践路径并能直接在本仓库中编译运行这些示例验证并行性能。一、示例总览原 README 以一张表格概括了parallel_reduce目录下的三个代码示例它们从易到难覆盖了该算法的主要使用形态示例名称说明convex_hull凸包算法的并行版本快速凸包 Quick Hullpi通过数值积分并行计算 πprimes埃拉托斯特尼筛法Sieve of Eratosthenes的并行版本三者恰好构成一条学习梯度pi最简单的用法直接对tbb::blocked_range做归约演示 Body 的最小协议primes进阶用法自定义 Range 类型、显式控制粒度并演示懒分裂优化convex_hull综合用法将parallel_reduce与parallel_for、concurrent_vector组合起来解决几何计算问题。在仓库中的实际目录结构为third-party/tbb/examples/parallel_reduce/ ├── convex_hull/ # 并行快速凸包 ├── pi/ # 并行数值积分求 π └── primes/ # 并行素数筛二、构建三个示例统一的 CMake 流程三个示例都遵循完全一致的 CMake 构建方式。在仓库中进入任一示例目录后执行cmake path_to_example cmake --build .以 pi 为例即cmake third-party/tbb/examples/parallel_reduce/pi cmake --build .从 pi/CMakeLists.txt 可以看到构建的最小配置cmake_minimum_required(VERSION 3.5.0...3.31.3) project(pi CXX) include(../../common/cmake/common.cmake) set_common_project_settings(tbb) add_executable(pi main.cpp pi.cpp) target_link_libraries(pi TBB::tbb Threads::Threads) target_compile_options(pi PRIVATE ${TBB_CXX_STD_FLAG})关键点通过TBB::tbb导入库链接 oneTBB同时链接Threads::Threads以启用系统线程支持main.cpp负责命令行解析、计时与串/并行版本调度算法本体位于pi.cpp两者分开编译CMake 要求版本 3.5.0 及以上兼容至 3.31.3并包含上层common/cmake/common.cmake公共配置。convex_hull 示例的构建略有特殊它额外提供两个预定义构建目标详见 convex_hull/README.mdconvex_hull_sample并行版本示例同时使用parallel_reduce、parallel_for与concurrent_vectorconvex_hull_bench对比串行与并行的有缓冲buffered/无缓冲unbuffered实现性能的基准版本。三、pi 示例parallel_reduce 的入门模板pi 示例通过数值积分计算 π将区间[0,1]划分为大量子区间在每个子区间上取中点的函数值近似积分累加所有切片结果后乘以步长。这是典型的可分治、可归约问题非常适合parallel_reduce。3.1 Body 协议的核心三要素parallel_reduce要求传入一个 Body 对象该对象必须满足归约协议。见 pi.cpp 中的reduce_bodystruct reduce_body { double my_pi; reduce_body() : my_pi(0) {} reduce_body(reduce_body x, tbb::split) : my_pi(0) {} void operator()(const tbb::blocked_rangenumber_t r) { my_pi pi_slice_kernel(r.begin(), r.size()); } void join(const reduce_body y) { my_pi y.my_pi; } };协议由三个成员构成operator()工作函数对传入的子区间执行局部计算并累加到本地状态my_pi。这里调用pi_slice_kernel(r.begin(), r.size())一次处理一个切片join()归约函数将另一个子任务的部分结果合并进当前 Body即my_pi y.my_pi分裂构造函数reduce_body(reduce_body x, tbb::split)当parallel_reduce决定把任务拆给另一个线程时用它构造出一个状态清零的副本。本例中my_pi(0)即新副本从零开始。随后在 pi.cpp 的compute_pi_parallel()中发起归约double compute_pi_parallel() { step pi_t(1.0) / num_intervals; double ret 0.0; reduce_body body; tbb::parallel_reduce(tbb::blocked_rangenumber_t(0, num_intervals), body); ret body.my_pi * step; return ret; }parallel_reduce将[0, num_intervals)这个blocked_range递归切分成多个子区间并行执行执行结束后每个线程的部分结果通过join()依次合并回根 Body最终body.my_pi即为所有切片的累加和再乘以步长step得到 π 的近似值。3.2 分块与向量化友好的切片内核切片大小chunk_size定义在 main.cpp 中const number_t chunk_size 4096; // Multiple of 16, to fit float datatype to a vector register.注释说明4096是 16 的倍数便于数据对齐到向量寄存器以获得更好的自动向量化效果。每个切片的求和内核pi_slice_kernel与单点求值pi_kernel位于 common.hinline pi_t pi_kernel(number_t i) { pi_t dx (pi_t(i) pi_t(0.5)) * step; return pi_t(4.0) / (pi_t(1.0) dx * dx); } inline double pi_slice_kernel(number_t slice, number_t slice_size chunk_size) { pi_t pi pi_t(0.0); for (number_t i slice; i slice slice_size; i) { pi pi_kernel(i); } return pi; }这里采用的是中点矩形法dx (i 0.5) * step是第i个子区间的中点4.0 / (1.0 dx*dx)是函数f(x) 4/(1x²)在中点的取值。每个切片独立求和切片的初始位置由r.begin()给出——这正是blocked_range把区间按连续块切分、保证每个operator()调用都能自包含完成局部计算的体现。3.3 线程数控制与串并行对照线程数通过tbb::global_control限制见 pi.cppstatic std::unique_ptrtbb::global_control gc; threading::threading(int p) { gc.reset(new tbb::global_control(tbb::global_control::max_allowed_parallelism, p)); } threading::~threading() { gc.reset(); }main()main.cpp中有一个值得注意的设计p 0表示运行串行版本便于在同一程序中公平对比串并行耗时for (int p threads.first; p threads.last; p threads.step(p)) { pi_t pi; double compute_time; if (p 0) { //run a serial version pi compute_pi_serial(); ... } else { //run a parallel version threading tp(p); pi compute_pi_parallel(); ... } }串行版本compute_pi_serial()用同一份切片内核按顺序累加并在末尾处理num_intervals % chunk_size的尾块从而与并行版本保持完全一致的计算语义。计时统一使用tbb::tick_count避免引入额外的外部依赖。四、primes 示例自定义 Range 与懒分裂进阶primes 示例统计[2, n]区间内的素数个数算法是经过效率优化的埃拉托斯特尼筛法。它在 primes.cpp 的文件头注释中明确写道The parallel version demonstrates how to useparallel_reduce, and in particular how to exploit lazy splitting.即本示例的核心教学点正是如何利用懒分裂lazy splitting。这是parallel_reduce最深入的用法之一值得单独剖析。4.1 串行版本分段筛网串行版SerialCountPrimes先把[0, n]分成筛网窗口Multiples类预筛出小于sqrt(n)的所有素数作为因子再在后续窗口内批量划掉合数。窗口大小为m sqrt(n)向上取整到偶数见 primes.cpp 的Multiples构造函数。4.2 并行版本的三层结构并行版ParallelCountPrimes由三层协作完成Multiples持有筛因子与打击指针striker。它的分裂构造函数只复制共享的因子指针my_factor而把工作缓存置空Multiples(const Multiples f, oneapi::tbb::split) : n_factor(f.n_factor), m(f.m), my_is_composite(nullptr), my_striker(nullptr), my_factor(f.my_factor) {}子任务被切分后并不立即准备数据而是推迟到真正开始处理某个窗口时才调用initialize(start)按需建立自己的筛网——这就是懒初始化。SieveRange自定义 Rangeparallel_reduce允许传入自定义 Range 类型只需实现四个签名primes.cpp 中注释为 Begin signatures required by parallel_reducebool is_divisible() const { return my_end - my_begin my_grainsize; } bool empty() const { return my_end my_begin; } SieveRange(SieveRange r, oneapi::tbb::split) { ... /* 取中点并对齐到窗口边界 */ }is_divisible()判断区间宽度是否仍大于粒度my_grainsizeempty()判断区间是否为空分裂构造函数从父 Range 的中点切开并将中点对齐到筛网窗口大小my_stride的整数倍保证每个子窗口都从合法的边界开始。SieveBodyoperator()处理一段连续窗口join()除合并count外还通过multiples.move(other.multiples)把右侧子任务的筛网状态接力给左侧 Body——注释指出这是因为parallel_reduce总是从左到右应用*this必须保持 striker 指针连续推进才能正确筛掉后续窗口中的合数。4.3 为什么使用 simple_partitionerParallelCountPrimes的调用方式是oneapi::tbb::parallel_reduce(SieveRange(s.multiples.m, n, s.multiples.m, grain_size), s, oneapi::tbb::simple_partitioner());这里显式传入simple_partitioner而不是默认的自动分区器。源码注释给出了明确理由Explicit grain size andsimple_partitioner()used here instead of automatic grainsize determination because we wantSieveRangeto be decomposed down to grainSize or smaller. Doing so improves odds that the working set fits in cache when evaluatingSieve::operator().即显式粒度 simple_partitioner可以保证SieveRange被切分到不超过grain_size的子块从而让每个线程处理时的工作集更可能驻留于缓存减少访存开销。默认 grain size 为 1000定义在 primes/main.cpp 的ParseCommandLine中可通过命令行参数覆盖。4.4 多线程调度与重复测量main.cpp 中的主循环对每个线程数p重复执行n-of-repeats次计算并使用utility::measurements统计耗时当重复次数大于 1 时还会调用utility::report_relative_error输出相对误差用于评估计时结果的稳定性if (options.repeatNumber 1) { par_info Relative_Err : ; utility::report_relative_error(measurements.computeRelError(), par_info.str()); }ParallelCountPrimes的默认参数在 primes.hpp 中声明numberOfThreads utility::get_default_num_threads()、grainSize 1000与命令行解析保持一致。五、convex_hull 示例多算法组合的快速凸包convex_hull 实现的是并行快速凸包算法Quick Hull。与 pi、primes 单一算法不同它综合运用了parallel_reduce、parallel_for与concurrent_vector且默认问题规模较大默认numberOfPoints 5000000见 convex_hull.hpp 的cfg命名空间namespace cfg { // convex hull problem user set parameters long numberOfPoints 5000000; // problem size // convex hull grain sizes for 3 subproblems. Be sure 16*GS 512Kb const std::size_t generateGrainSize 25000; const std::size_t findExtremumGrainSize 25000; const std::size_t divideGrainSize 25000; }; // namespace cfg值得留意的是注释中的约束16 * GS 512Kb——三个子问题的粒度generateGrainSize、findExtremumGrainSize、divideGrainSize都取 25000目的是让每个子任务的工作集按 16 字节的点结构估算保持在 L2 缓存容量之下。这体现了 oneTBB 示例中粒度设计要服务于缓存友好性的一贯思路。算法流程上Quick Hull 会反复执行找极值点、按极值把点集划分为左右两个子集的递归步骤找极值适合用parallel_reduce在子点集中求距离最远点划分子集适合用parallel_for并行过滤点点集的存放则使用concurrent_vector以支持并发写入。三者组合形成了对 oneTBB 容器与算法 API 的完整演示。命令行解析同样位于 convex_hull.hpp 的ParseInputArgs它额外处理silent/verbose标志并约定当指定silent时自动关闭verbose。六、运行示例make 目标与命令行参数6.1 统一的位置参数与关键字参数语法三个示例共享同一套 oneTBB 示例参数约定既支持keyvalue形式也支持纯位置参数形式。每个示例都隐式支持-h帮助选项若想查看完整用法直接运行-h即可。pi 示例的用法摘自 pi/README.mdpi [n-of-threadsvalue] [n-of-intervalsvalue] [silent] [-h] [n-of-threads [n-of-intervals]]primes 示例的用法摘自 primes/README.mdprimes [n-of-threadsvalue] [numbervalue] [grain-sizevalue] [n-of-repeatsvalue] [silent] [-h] [n-of-threads [number [grain-size [n-of-repeats]]]]convex_hull 的用法摘自 convex_hull/README.mdconvex_hull_sample [n-of-threadsvalue] [n-of-pointsvalue] [silent] [verbose] [-h] [n-of-threads [n-of-points]] convex_hull_bench [n-of-threadsvalue] [n-of-pointsvalue] [silent] [verbose] [-h] [n-of-threads [n-of-points]]6.2 通用参数说明参数含义约束-h打印命令行选项帮助—n-of-threads使用的线程数格式为low[:high]low 与可选的 high 均为非负整数也可用auto表示平台默认线程数0表示运行串行版本pi、primes 支持silent除耗时外不输出其他内容pi 中 silent 模式下仍输出相对误差primesverbose打开详细输出仅 convex_hull 支持与 silent 互斥各示例独有参数参数适用示例含义约束n-of-pointsconvex_hull随机生成的点数非负整数n-of-intervalspi积分区间的划分数必须为正整数numberprimes素数搜索范围上界[2, number]必须为正整数grain-sizeprimes筛网分解的粒度可选必须为正整数n-of-repeatsprimes计算重复次数必须为正整数大于 1 时计算相对误差各示例的默认值从源码确认pi 默认num_intervals 100000000010 亿个区间见 main.cppprimes 默认number 100000000、grainSize 1000、repeatNumber 1见 main.cppconvex_hull 默认numberOfPoints 5000000见 convex_hull.hpp。6.3 预定义 make 目标构建完成后可通过 make 目标一键运行三个示例共用一套命名规范pi见 pi/CMakeLists.txtmake run_pi以预定义参数运行make perf_run_pi以推荐参数运行以测量 oneTBB 性能。其参数在 CMake 中定义为PERF_ARGS auto 100000000000即使用平台默认线程数、划分 1000 亿个区间。primesmake run_primes以预定义参数运行make perf_run_primes以推荐参数运行测量性能make benchmark_primes以推荐参数重复多次测量并报告相对误差make benchmark_primes_data同benchmark_primes但把结果写入benchmark_primes_data.csv文件。convex_hullmake run_convex_hull以预定义参数运行make perf_run_convex_hull以推荐参数运行测量性能make light_test_convex_hull以精简参数运行缩短执行时间适合快速冒烟验证。add_execution_target宏将这些目标注册到 CMake 构建系统中因此上述 make 目标在cmake --build .之后即可直接使用。七、从示例到实战parallel_reduce 使用要点小结综合三个示例可以归纳出parallel_reduce的几条实战经验供在 mold 仓库中或自己的项目中迁移使用Body 必须满足三件套协议operator()完成子区间局部计算、join()合并部分结果、分裂构造函数接受tbb::split参数创建干净的副本。pi 的 reduce_body 是最小的可运行模板。归约操作需满足结合律与交换律的近似语义浮点求和顺序不同会导致结果有微小差异示例通过串并行共用同一pi_slice_kernel切块逻辑来尽量缩小差异。自定义 Range 时实现四个签名empty()、is_divisible()、分裂构造函数和begin()/end()访问器若切分边界有对齐要求如 primes 的窗口筛务必在分裂构造函数中做对齐处理。粒度是性能杠杆默认自动分区器适合多数场景当希望严格把子任务切到固定大小以改善缓存局部性时使用显式 grain size simple_partitioner参考 primes 的调用方式。懒分裂避免无用初始化在分裂构造函数中只拷贝共享元数据、推迟重型缓存分配如 primes 的Multiples分裂构造 initialize()可以显著降低任务切分的开销。线程数上限用global_control控制通过tbb::global_control(tbb::global_control::max_allowed_parallelism, p)在运行时限制并行度并可用p 0约定串行模式来做公平基准对比pi、primes 均采用此约定。计时使用tbb::tick_count三个示例统一用它计时避免引入平台相关的chrono差异配合n-of-repeats与相对误差报告可以得到更稳定的性能数据。如果想验证这些结论可以直接在本仓库中依次构建并运行pi、primes、convex_hull三个示例观察串行与并行版本的耗时差异并尝试调整n-of-threads、grain-size等参数观察性能变化——这正是parallel_reduce从理论走向实践的最快路径。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考