mold 内置 oneTBB:把 flow graph 钉到指定核心,task_arena 绑定与 reset 重挂载
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
一、问题现场
混合架构机器上,一张跑批用的 flow graph 全落在 E-core,单节点延迟翻了三倍。任务明明是从主线程 try_put 进去的,调度器却按默认策略摊到全部核心上。想挪窝只有两条路:构造时进目标 arena,或运行期 reset() 重挂载。
二、一句话讲清机制
task_arena(任务竞技场)是调度器里的工位区:线程池被划出若干隔离块,每块有自己的核心偏好和并发上限。graph(数据流图)激活时会挂到激活线程所在的那块工位区,之后凡是这张图名义上派发的任务,一律落到图挂着的工位,跟谁调用 try_put 没关系。类比:任务跟着图的工位走,不跟叫门的人走。
对照仓库示例 flow_graph_examples.cpp 的 L29-L42,最小可运行绑定片段是:
tbb::task_arena arena( tbb::task_arena::constraints{}.set_core_type(core_types.back()) ); arena.execute( [&]() { flow::graph g; flow::function_node<int> f(g, flow::unlimited, [](int) { /* 落在首选核心类型上 */ }); f.try_put(1); g.wait_for_all(); } );图 g 在回调内构造,激活即附着到受约束 arena,函数节点回调随之后落在首选核心类型上。
三、绑定时机对照表
| 维度 | 构造期绑定 | reset() 运行期绑定 |
|---|---|---|
| 触发时机 | graph 激活瞬间,附着到激活线程所在 arena | 在目标 arena 的 execute 回调里调用 g.reset() |
| 适用场景 | 图随任务生灭的一次性计算 | 图是成员变量或长期对象,需要中途迁 arena |
| 生命周期约束 | 图必须活在 execute 回调或同 arena 线程内 | 无约束,reset 可反复执行 |
| 是否需要 reset | 不需要 | 需要,且会一并清空节点状态 |
构造期绑定最省事:图、节点、消息全活在一次 execute() 里,回调返回时图随作用域销毁,不存在残留状态。
reset() 绑定适合图作为长期对象存在的结构,代价是它顺手清空节点状态,不能当纯"搬家"操作使用。
四、源码走读:reset() 重绑定 arena 的五步
翻开 flow_graph.h 第 598 行,graph::reset(reset_flags) 一共 16 行,五步分工明确:
inline void graph::reset( reset_flags f ) { deactivate_graph(*this); // ← 第一步:停用图,停止接收新消息 my_context->reset(); // ← 第二步:重置任务组上下文 cancelled = false; // 清掉取消标志 caught_exception = false; // 清掉异常标志 for(iterator ii = begin(); ii != end(); ++ii) { (&(*ii))->reset_node(f); // ← 第三步:遍历节点,逐一恢复缓存与计数 } prepare_task_arena( true ); // ← 第四步:reinit 模式,附着当前线程的 arena activate_graph(*this); // ← 第五步:重新激活,任务开始落新 arena }真正决定"搬到哪"的只有第四步:prepare_task_arena 以 reinit 模式读取调用线程所在的 arena,替换图内的 my_task_arena 指针,随后第五步让图对新 arena 重新可见。
五、constraints 参数速查:core_type 与 NUMA 亲和
| 参数名 | 作用 | 典型取值 | 注意事项 |
|---|---|---|---|
| core_type | 首选核心类型(P-core / E-core) | tbb::info::core_types().back() 取最强 | 单核类型平台列表只有一项,设置无意义 |
| numa_id | 首选 NUMA 节点 | tbb::info::numa_nodes() 中的索引 | 受进程 affinity mask 影响,被剔除的节点不会列出 |
| max_threads_per_core | 每核最大并发线程数,用于关超线程 | 1 | 只减并发不增吞吐,配 default_concurrency 查并发数更省开销 |
三个约束可链式叠加,一次给全:
auto c = tbb::task_arena::constraints{} .set_numa_id(1) .set_core_type(core_types.back()) .set_max_threads_per_core(1); tbb::task_arena arena(c);六、容易踩的坑
现象:tbb::info::numa_nodes() 返回的节点数比 numactl 看到的少,约束设置后 arena 线程数与预期不符。原因:tbb::info 接口尊重进程 affinity mask,被亲和性排除的节点直接从列表消失。规避:拿不到目标 numa_id 时先检查 taskset 设置,约束值只在返回列表范围内有效。
现象:长期存活的图 reset 到目标 arena 后,输出节点的计数、节点内缓存消息消失,下游行为变化。原因:reset() 对每个节点调 reset_node,清的是节点状态,不只是 arena 指针。规避:需要保留中间结果时,先把结果搬到图外再执行 reset。
现象:等待图完成的线程顺手执行了别的任务,线程局部变量被外层迭代改写,断言偶发失败。原因:默认无隔离时,等待线程会"顺手"领走 arena 内其他任务,同一线程上内外层交错执行。规避:用 this_task_arena::isolate 限定等待线程只处理隔离区任务,或把内层构造放进独立 arena。
七、30 秒速查
- graph 附着发生在激活时,落在激活线程当前所在 arena。
- 构造期绑定:把 graph 写进 arena.execute 回调,一次性图最省事。
- 运行期迁移:在目标 arena 回调里 g.reset(),底层是 prepare_task_arena(reinit)。
- 图内任务跟随图的附着点执行,与 try_put 调用线程无关。
- core_type / numa_id / max_threads_per_core 用 task_arena::constraints 链式设置,取值受 affinity mask 约束。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考