
oneTBB ParallelForIndex 命名需求解析parallel_for 对索引类型的完整契约与 mold 中的实战印证【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读本文围绕 oneTBBoneAPI Threading Building Blocks规范文档中定义的ParallelForIndex 命名需求Named Requirement展开系统梳理oneapi::tbb::parallel_for对索引类型Index的全部约束从构造、赋值到加减乘除运算从D类型语义到推荐使用整型的原因。文章同时结合 oneTBB 头文件源码concept 约束、blocked_range实现、迭代包装器以及当前仓库中 mold 链接器对tbb::parallel_for的真实调用帮助读者理解这些运算符号在并行分治机制中各自的角色并掌握编写正确、可向量化的并行索引循环的实战要点。1. 什么是 Named RequirementParallelForIndex 的定位oneTBB 的规范文档位于 third-party/tbb/doc/main/specification/source/named_requirements/algorithms/par_for_index.rst使用命名需求来描述模板算法的类型约束。ParallelForIndex规范锚点[req.parallel_for_index]是其中专门服务于parallel_for整数索引重载的一组需求A typeIndexsatisfiesParallelForIndexif it meets the following requirements.它规定了当Index类型满足ParallelForIndex时才能作为oneapi::tbb::parallel_for(first, last, step, f)这类重载的索引类型使用。与之配套的还有ParallelForFunc函数对象需求和ParallelForBody范围体需求三者共同构成parallel_for的完整类型契约。2. ParallelForIndex 需求清单伪签名与语义原文档以伪签名 语义的形式给出了Index类型必须支持的 12 项操作。下表完整继承该清单并补充了每项操作在并行迭代中的实际用途伪签名语义在 parallel_for 中的角色Index::Index(int)从int值构造支持parallel_for内部将步进索引规整为blocked_rangeIndex的起点Index::Index(const Index)拷贝构造parallel_for会为每个子范围拷贝Range与BodyIndex::~Index()析构生命周期管理Index operator(const Index)赋值子范围切分时的值回填Index operator()推进到下一个值串行/向量化循环中的步进ibool operator(const Index i, const Index j)i是否先于j循环终止判断i last、empty()判断bool operator(const Index i, const Index j)i先于或等于j范围边界比较D operator-(const Index i, const Index j)区间[i,j)内的值个数计算范围大小、迭代次数与切分点Index operator(const Index i, const Index j)i与j之和计算begin b * step等偏移Index operator(const Index i, D k)i之后第k个值子范围起点计算Index operator*(const Index i, const Index j)i与j之积由子块编号 × 步长还原真实索引Index operator/(const Index i, const Index j)i与j之商由范围长度计算块数(last-first-1)/step 12.1 关于D类型减法结果必须是可转换的整数原文档特别强调Dis the type of the expressionj-i. It can be any integral type that is convertible tosize_t.即Index相减的结果类型D不必与Index相同但必须是可转换为size_t的整数类型。这是因为parallel_for内部最终要把迭代次数交给blocked_range管理而blocked_range的size_type正是std::size_t。在 blocked_range.h 中可以看到using size_type std::size_t; size_type size() const { __TBB_ASSERT( !(end()begin()), size() unspecified if end()begin() ); return size_type(my_end-my_begin); }size()直接执行my_end - my_begin并转换为size_type因此若j-i的结果无法无损转换为size_t就会出现截断或符号问题。这解释了为什么规范对D的可转换性如此强调。2.2 合法的 Index 模型整型与指针原文档指出建模Index的典型类型是整型integral types和指针pointers。指针天然满足全部需求p、p q、q - p返回ptrdiff_t、p n等运算齐备且ptrdiff_t可转换为size_t。不过文档随后给出明确建议It is recommended to use integral types asParallelForIndex.理由不难从实现推断整型迭代在 parallel_for.h 的parallel_for_body_wrapper::operator()中可以直接被编译器向量化而指针索引会引入额外的寻址计算。wrapper 中甚至为 Intel 编译器显式写入了#pragma ivdep忽略依赖提示目标就是让内层循环生成 SIMD 代码void operator()( const blocked_rangeIndex r ) const { Index b r.begin(); Index e r.end(); Index ms my_step; Index k my_begin b*ms; for ( Index i b; i e; i, k ms ) { tbb::detail::invoke(my_func, k); } }注意这里my_begin b*ms用到了需求表中的operator与operator*i e用到了operatori用到了operator——需求表中的每一项都在运行路径上真实发生。3. C20 时代的编译期检查parallel_for_indexconcept在支持 C20 的构建下oneTBB 将这套命名需求直接表达为概念concept位于 parallel_for.htemplate typename Index concept parallel_for_index std::constructible_fromIndex, int std::copyableIndex requires( const std::remove_reference_tIndex lhs, const std::remove_reference_tIndex rhs ) { { lhs rhs } - adaptive_same_asbool; { lhs - rhs } - std::convertible_tostd::size_t; { lhs (rhs - lhs) } - std::convertible_toIndex; };对照原文档可以一一对应std::constructible_fromIndex, int↔Index::Index(int)std::copyableIndex↔ 拷贝构造 赋值 析构{ lhs rhs } - adaptive_same_asbool↔operator{ lhs - rhs } - std::convertible_tostd::size_t↔D operator-且要求结果可转换为size_t{ lhs (rhs - lhs) } - std::convertible_toIndex↔Index operator(const Index, D)其中rhs - lhs正是D类型。概念只检查了运行路径上最关键的三个表达式比较、减法、加法而operator*、operator/、operator、operator等仍以未显式检查的隐式需求存在——概念检查通过 ≠ 所有需求满足这是命名需求与概念并存的现实规范文档定义完整契约concept 提供可落地的编译期约束子集。4. 与 parallel_for 算法的配合等价循环与语义约束4.1 三步重载parallel_for(first, last, step, f)按 parallel_for 算法文档parallel_for(first, last, step, f)表示如下循环的并行执行for (auto i first; i last; i step) f(i);文档同时给出三条硬约束循环不得回绕wrap aroundlast必须严格大于first且迭代方向单调不允许索引越过端点折返step 必须为正缺省时隐式为1。源码在parallel_for_impl中强制检查 parallel_for.hif (step 0 ) throw_exception(exception_id::nonpositive_step); // throws std::invalid_argument else if (first last) { Index end Index(last - first - 1ul) / step Index(1); blocked_rangeIndex range(static_castIndex(0), end); parallel_for_body_wrapperFunction, Index body(f, first, step); parallel_for(range, body, partitioner); }注意迭代次数(last - first - 1) / step 1正是依赖需求表里的operator-与operator/计算出来的随后又把[0, end)区间交给blocked_range由后者按grainsize递归切分。不得依赖并行执行文档明确没有保证迭代会真正并行执行并且警告若一个较小靠前的迭代等待一个较大靠后的迭代完成可能发生死锁。这意味着f(i)内部不能自旋等待f(j)j i的结果。4.2 范围重载parallel_for(range, body)parallel_for的另一族重载接收Range与Body语义是对range中的每个值执行body。运行机制是递归地将范围切分到is_divisible()为假为每个子范围拷贝一份Body再调用Body::operator()(subrange)。其中Range必须满足 Range 命名需求Body必须满足 ParallelForBody 需求。整数索引重载正是把索引区间包装成blocked_rangeIndex后走这条通用路径实现的。4.3 函数对象需求ParallelForFunc与Index配套的Func类型必须满足 ParallelForFunc 需求void F::operator()(Index index) const;Index类型必须与parallel_for的对应模板参数一致并且operator()应为constconst Function my_func被包装进parallel_for_body_wrapper后还会被逐个子范围拷贝。mold 中大量使用的 { ... }无捕获或引用捕获 lambda 正是该需求的典型实现。4.4 执行顺序与复杂度执行顺序不确定parallel_for可能以非确定顺序执行迭代正确性不得依赖任何特定顺序但出于效率考量可以预期它倾向于对连续的值区间进行操作这是blocked_range分治切分的自然结果。串行执行语义在串行回退路径上parallel_for按从左到右的顺序执行迭代。复杂度若 Range 与 Body 均占O(1)空间且范围近似均匀切分则空间复杂度为O(P·log N)其中N为范围大小P为线程数。延迟析构部分 Range/Body 拷贝可能在parallel_for返回后才被销毁追踪执行轨迹或编写带复杂副作用的 Body 时需留意这一点。4.5 分区器与任务上下文parallel_for的完整签名允许传入分区器partitioner与 task_group_contextvoid parallel_for(Index first, Index last, const Func f, /* 分区器 */, task_group_context context);分区器可取const auto_partitioner、const simple_partitioner、const static_partitioner、affinity_partitioner之一不指定时默认使用auto_partitioner。task_group_context参数使算法任务在指定上下文中执行缺省则运行在算法自身的绑定上下文。对应源码中的 24 个重载组合见 parallel_for.h。5. 底层分治机制start_for 任务树从源码结构看parallel_for的实际执行载体是 start_for 任务类型templatetypename Range, typename Body, typename Partitioner struct start_for : public task { Range my_range; const Body my_body; typename Partitioner::task_partition_type my_partition; ... start_for( start_for parent_, typename Partitioner::split_type split_obj, ... ) : my_range(parent_.my_range, get_range_split_objectRange(split_obj)), my_body(parent_.my_body), ...它通过分裂构造splitting constructor生成左右子任务offer_work把右子任务挂到新建的tree_node上并 spawn形成一棵任务树最后execute()中调用my_partition.execute(*this, my_range, ed)驱动分区器逐步切分、执行run_body。这棵树的每一次切分都会触发Range的分裂构造——而blocked_range的分裂正是基于operator-取中点与operator断言切分正确实现的再次印证了命名需求中每个运算符的必要性。6. mold 中的实战印证链接器如何用整型索引并行化当前仓库mold一款现代链接器将 oneTBB 作为并行运行时内嵌于third-party/tbb并在核心链接流程中大量使用tbb::parallel_for其Index统一选用i64即 64 位整型完全符合推荐使用整型的规范建议。例如arch-arm32.cc对num_entries个重定位条目并行执行tbb::parallel_for((i64)0, num_entries, {...})gc-sections.cc对ctx.objs.size()个目标文件做并行标记tbb::parallel_for((i64)0, (i64)ctx.objs.size(), {...})gdb-index.cc并行构建 GDB 索引时对 CU 列表执行同样的整数区间并行。这些调用模式与本文所述的parallel_for(first, last, f)缺省 step1、缺省auto_partitioner完全一致Index i64内置整型自动满足ParallelForIndex、Func 捕获引用参数的 lambda满足ParallelForFunc。这为整型 lambda的组合提供了来自真实大型 C 项目的可复现范例。7. 相关命名需求速览ParallelForIndex并非孤立存在它与以下命名需求共同构成 oneTBB 并行算法的类型契约体系均位于 named_requirements/algorithms 目录命名需求约束对象核心要点ParallelForIndexIndex构造/赋值/比较/加减乘除D可转size_tParallelForFuncFuncvoid F::operator()(Index) constParallelForBodyBody对子范围执行void Body::operator()(Range) constRangeRangeis_divisible()/empty()/分裂构造BlockedRangeValueValue拷贝/赋值//-/示例含整型、指针、STL 随机访问迭代器其中BlockedRangeValue与ParallelForIndex需求高度同构都要求D j-i可转换为size_t因为blocked_rangeValue的size_type即为size_t区别在于BlockedRangeValue额外接受STL 随机访问迭代器其difference_type可隐式转换为size_t而ParallelForIndex面向的是parallel_for的索引语义。8. 实用建议与常见陷阱综合规范文档与源码实现编写使用parallel_for(first, last, step, f)的代码时请注意优先使用整型索引如std::int64_t既满足需求又利于编译器向量化wrapper会为 Intel 编译器注入ivdep提示前提是循环体内无跨迭代依赖。step 必须为正否则运行时抛出std::invalid_argument源码exception_id::nonpositive_stepstep 缺省为 1。迭代范围不得回绕first last是进入实际并行路径的前提parallel_for_impl中first last为假时直接跳过。不要依赖执行顺序也不要在f(i)内等待更大索引的迭代结果否则可能死锁。注意延迟析构parallel_for返回后部分 Range/Body 拷贝可能仍存活避免在其析构中执行关键副作用。自定义 Index 类型时如带刻度的日期类型务必同时提供operator-返回可转size_t的差、operator含IndexD形式、operator*、operator/、operator并保证operator与加一语义一致——它们在迭代计数、范围切分、索引还原三个环节都会被调用。参考资料仓库内规范原文par_for_index.rst、par_for_func.rst算法文档parallel_for_func.rst源码实现parallel_for.hconcept 定义于 L42-L49wrapper 于 L183-L209parallel_for_impl于 L305-L316start_for于 L60-L146范围实现blocked_range.hmold 真实用法arch-arm32.cc、gc-sections.cc、gdb-index.cc【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考