mold 仓库内嵌 oneAPI TBB 指南:enumerable_thread_specific 的 combine 与 combine_each 合并机制
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
本文围绕 mold 仓库内嵌的 oneAPI TBB(oneAPI Threading Building Blocks)规范文档 combining.rst 展开,系统讲解线程本地容器enumerable_thread_specific提供的两个串行合并接口:二元归约combine()与逐元素访问combine_each()。读完本文,你将掌握这两个接口的签名约束、空容器语义、底层实现细节,以及它们与combinable的委托关系,并能在并行归约场景中正确选用。
enumerable_thread_specific(下称 ETS)是 oneAPI TBB 提供的线程本地存储容器:每个访问它的线程拥有一份独立的T类型副本,副本惰性创建;容器整体又可通过迭代器与范围被当作一个普通容器使用。当并行计算结束后,需要把散落在各线程本地副本中的部分结果"收拢"为最终结果时,就轮到本规范文档所定义的合并接口登场。本仓库中该容器的完整声明见 enumerable_thread_specific_cls.rst,其源码实现位于 enumerable_thread_specific.h。
一、合并接口的定位与总体约定
规范文档开宗明义地指出:
The member functions in this section iterate across the entire container sequentially in the calling thread.
即本节(Combining)的所有成员函数都在调用线程内串行地遍历整个容器。这意味着合并阶段是纯串行的归约过程,不会引入额外的并行度,因此二元/一元函数对象被假定为"非良性"(non-benign),即允许携带副作用。
ETS 类模板签名(默认参数)为:
template <typename T, typename Allocator = cache_aligned_allocator<T>, ets_key_usage_type ETS_key_type = ets_no_key> class enumerable_thread_specific;容器头部注释对combine/combine_each给出了三条重要约定(见 enumerable_thread_specific.h):
- 两个方法均为 ETS 定义;其中
combine()要求类型T定义有operator=(); - 两者都不会修改容器内容(但不保证传入的函数对象本身不去修改元素);
- 两者都在串行上下文中求值(默认视为非良性操作)。
二、combine():跨线程副本的二元归约
签名与要求
template<typename BinaryFunc> T combine(BinaryFunc f);按规范文档,BinaryFunc必须满足 ISO C++ 标准 [function.objects] 一节描述的Function Objects要求,且具体应为结合律(associative)二元函数对象,签名形如T BinaryFunc(T,T)或T BinaryFunc(const T&,const T&)。参数T必须与enumerable_thread_specific的模板参数一致。
"结合律"这一要求至关重要:combine只是顺序地把元素两两合并,并不承诺任何特定合并次序,因此只有满足f(f(a,b),c) == f(a,f(b,c))的函数(如加法、乘法、std::plus)才被允许。这与oneapi::tbb::parallel_reduce对归约函数的要求一脉相承。
语义:归约、空容器与初值
规范给出三条语义:
- Effects:使用二元函数对象
f对所有元素计算归约(reduction); - 空容器:若没有任何元素,则按照"创建线程本地元素"的同一套规则生成结果(即用默认构造、
Finit初始化函数或 exemplar 副本构造出结果); - Returns:返回归约结果。
源码实现解析
enumerable_thread_specific.h 中的实现与规范完全对应:
// CombineFunc has signature T(T,T) or T(const T&, const T&) template <typename CombineFunc> T combine(CombineFunc f_combine) { if(begin() == end()) { ets_element<T> location; my_construct_callback->construct(location.value()); return *location.value_committed(); } const_iterator ci = begin(); T my_result = *ci; while(++ci != end()) my_result = f_combine( my_result, *ci ); return my_result; }从实现可见两个关键细节:
- 空容器路径:当
begin() == end()时,构造一个临时的ets_element<T>,经由my_construct_callback->construct(...)按创建线程本地副本的同一回调规则构造元素并立即返回其值。这也解释了为何combine()要求T可拷贝/可赋值(operator=)——归约过程my_result = f_combine(...)依赖赋值操作。 - 非空路径:以首元素
*ci作为归约初值my_result,随后按内部存储顺序(底层my_locals是concurrent_vector,见 enumerable_thread_specific.h)逐一执行f_combine(my_result, *ci)。由于二元函数满足结合律,该顺序不影响最终结果。
实战示例
#include <oneapi/tbb/enumerable_thread_specific.h> #include <oneapi/tbb/parallel_for.h> #include <oneapi/tbb/blocked_range.h> #include <numeric> // 每个线程累加自己的部分和 oneapi::tbb::enumerable_thread_specific<long long> sums{0LL}; oneapi::tbb::parallel_for( oneapi::tbb::blocked_range<int>(0, N), & { long long& s = sums.local(); for (int i = r.begin(); i < r.end(); ++i) s += i; }); // 串行归约出最终和;空容器时返回 0(由初始化函数 finit 决定) long long total = sums.combine(std::plus<long long>{});测试套件中同样大量使用该模式,例如 conformance_enumerable_thread_specific.cpp 分别以值语义与引用语义的加法函数对象调用combine,并断言归约结果等于期望和;conformance_combinable.cpp 则验证了combinable在 copy/assign/move 之后combine结果依然正确,以及clear()后归约结果归零。
三、combine_each():对每个本地副本逐一求值
签名与要求
template<typename UnaryFunc> void combine_each(UnaryFunc f);UnaryFunc同样须满足 [function.objects] 的 Function Objects 要求,具体为一元函数对象,接受下列任一签名:
void UnaryFunc(T) // 按值接收 void UnaryFunc(T&) // 可变引用接收 void UnaryFunc(const T&) // 常量引用接收T必须与 ETS 的模板参数一致。
语义:副作用驱动的遍历
规范定义Effects为:对*this中的每个实例x求值f(x)。与combine()不同,combine_each()不产生归约结果(返回void),它的价值在于允许对每个线程本地副本执行任意副作用操作——累加到外部累加器、清空副本、统计数量等。
源码实现解析
enumerable_thread_specific.h 的实现极简:
// combine_func_t takes T by value or by [const] reference, and returns nothing template <typename CombineFunc> void combine_each(CombineFunc f_combine) { for(iterator ci = begin(); ci != end(); ++ci) { f_combine( *ci ); } }逐元素地把本地副本的引用*ci传给f_combine。由于传入的是引用,采用T&签名的函数对象可以就地修改各线程本地副本;combine()则无法做到这一点(它只读地合并到局部变量)。测试 conformance_enumerable_thread_specific.cpp 专门验证了combine_each通过T&修改本地值的能力,并断言 "combine_each does not allow to modify thread local values" 的反例不成立。
实战示例:累加并清空
long long total = 0; // 第一步:把每个线程的部分和累加进 total sums.combine_each(& { total += v; }); // 第二步:就地清空所有本地副本 sums.combine_each(& { v = 0; }); // 第三步:验证已被清空 sums.combine_each(& { assert(v == 0); });这正是 conformance_enumerable_thread_specific.cpp 中Accumulator/ClearingAccumulator/AssertClean三连调用的真实写照:先累加,再累加并清空,最后断言副本全部干净。
四、combine与combine_each的选型对比
| 维度 | combine(f) | combine_each(f) |
|---|---|---|
| 函数对象 | 二元,需满足结合律 | 一元,无结合律要求 |
| 返回值 | T(归约结果) | void |
| 空容器行为 | 按创建规则构造一个临时结果并返回 | 循环体不执行,直接返回 |
是否要求operator= | 是 | 否 |
| 修改本地副本 | 不能(只读合并) | 能(T&签名) |
| 典型用途 | 求和、求积、取极值等归约 | 汇总到外部变量、清空、统计 |
选型原则很直接:需要"一个合并后的值"用combine;需要对每个副本做副作用操作(或就地修改)用combine_each。两者均串行遍历、均不修改容器结构本身。
五、与combinable的委托关系
oneAPI TBB 还提供了更轻量的combinable容器,其完整规范见 combinable_cls.rst。从源码看,combinable本质上是 ETS 的薄封装(combinable.h):
template <typename T> class combinable { using my_ets_type = typename tbb::enumerable_thread_specific<T, my_alloc, ets_no_key>; my_ets_type my_ets; public: // ... template <typename CombineFunc> T combine(CombineFunc f_combine) { return my_ets.combine(f_combine); } template <typename CombineFunc> void combine_each(CombineFunc f_combine) { my_ets.combine_each(f_combine); } };因此本文对 ETScombine/combine_each的一切语义分析(要求、Effects、空容器规则、串行遍历)都直接适用于combinable。二者的差异在于:combinable固定使用ets_no_key的缓存策略与cache_aligned_allocator,模板参数只有T;而 ETS 还支持自定义分配器与ets_key_per_instance/ets_suspend_aware等底层实现选择。如果你只是需要一个"并行计算后合并"的临时容器,combinable更简洁;若还需要跨容器迭代、并行range()、分段迭代器等能力,则应直接使用 ETS。
六、注意事项与边界情况
- 串行执行语义:
combine/combine_each均在调用线程内顺序执行(规范明示),函数对象若被并行场景之外的代码复用,需自行保证其正确性;ETS 头文件注释也强调二者被视为"非良性"(non-benign)操作(enumerable_thread_specific.h)。 - 不修改容器:两个方法本身不改变 ETS 内容,
combine_each以T&传参时对副本的修改由用户函数对象负责,容器结构(元素集合)不变。 combine的拷贝/赋值要求:combine内部用my_result = f_combine(...)反复赋值,要求T可默认/按回调构造且可赋值;若T不可赋值,应改用combine_each聚合到外部变量。ETS 头文件明确:"the contained objects need not have operator=() defined if combine is not used"(enumerable_thread_specific.h)。- 线程标识复用:ETS 使用操作系统返回的
std::this_thread::get_id()区分线程,该值只在线程存续期内唯一,新建线程可能拿到已销毁线程的 ID,导致本地副本数可能少于实际访问线程数(见 enumerable_thread_specific_cls.rst 的 caution)。因此不要假设combine的元素个数等于应用线程数;测试 conformance_enumerable_thread_specific.cpp 展示了主线程与若干工作线程各自写入后,combine的期望值计算需把主线程副本一并计入。 - 结合律是正确性前提:
combine的遍历顺序取决于内部concurrent_vector的创建顺序(实现细节),规范并不承诺固定次序,归约函数必须满足结合律才能保证结果与顺序无关。
七、在 mold 仓库中的阅读指引
enumerable_thread_specific的合并接口是理解 TBB 线程本地归约管线的关键一环。在 mold 仓库中可按以下路径继续深挖:
- 规范文档:combining.rst、enumerable_thread_specific_cls.rst、combinable_cls.rst;
- 容器实现:enumerable_thread_specific.h(
combine/combine_each见第 1028–1049 行)、combinable.h; - 一致性测试:conformance_enumerable_thread_specific.cpp、conformance_combinable.cpp,其中覆盖了空容器、copy/move 后归约、
clear()后归零、combine_each就地修改等边界场景。
一句话总结:并行阶段用local()累积,串行收尾用combine()归约、用combine_each()做副作用处理,这就是 ETS 合并机制的完整用法闭环。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考