mold 仓库内置 oneTBB 的 Task Group Bypass 支持解析:用 task_handle 降低任务调度开销 mold 仓库内置 oneTBB 的 Task Group Bypass 支持解析用 task_handle 降低任务调度开销【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读本文基于 mold 仓库内置的 oneTBBIntel oneAPI Threading Building Blocks文档 task_bypass_support.rst深入讲解task_group的 Task Bypass任务旁路扩展如何通过让任务函数返回task_handle直接向调度器提示下一个该执行哪个任务从而省去入队、取队与偷取stealing带来的调度开销。读完本文你将掌握该扩展的启用方式、API 语义、约束边界、divide-and-conquer 实战写法以及从defer到调度器 bypass 循环的底层实现原理。说明mold 是一个现代链接器项目其仓库将 oneTBB 作为第三方依赖内嵌于third-party/tbb目录。本文聚焦该内嵌 oneTBB 中task_group扩展文档所定义的技术主题。背景为什么要旁路调度task_group是 oneTBB 面向任务的并行编程接口开发者用run/defer/run_and_wait提交任务调度器负责把任务分发给工作线程执行。默认的调度路径是spawn生成按照 How Task Scheduler Works 描述的规则当前线程 spawn 一个新任务需要经历三步把新任务推入线程的本地任务队列deque继续执行当前任务直到完成从线程队列取出下一个任务除非被其他线程偷走。该流程记录在 Task_Scheduler_Bypass.rst 中。第 1 步和第 3 步引入了不必要的队列操作更糟的是步骤 3 允许偷取这可能在并未增加并行度的情况下破坏数据局部性。**Task Scheduler Bypass任务调度旁路**正是针对这一开销的优化开发者直接指定下一个应该执行的任务由当前任务把该任务作为返回值交回调度器。根据调度规则返回的任务会成为当前线程下一个要执行任务的第一候选从而跳过入队/出队操作同时这种方式几乎可以保证任务由当前线程执行、而非被其他线程抢走。在当时的实现中使用这一优化的唯一途径就是oneapi::tbb::task_group的预览preview扩展——即本文介绍的 Task Bypass Support。启用条件预览宏开关Task Bypass Support 属于 oneTBB 的预览特性必须显式启用#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 #include oneapi/tbb/task_group.h宏必须在#include oneapi/tbb/task_group.h之前定义且取值为1。该开关同时控制着整个task_group扩展族见 task_group_extensions.rst除本文的 Task Bypass 外还包括task_completion_handle类、动态依赖dynamic dependencies与单任务等待wait_single_task等扩展。在源码中该宏对应__TBB_PREVIEW_TASK_GROUP_EXTENSIONS内部宏task_group.h、_task_handle.h等头文件中的扩展代码均被#if __TBB_PREVIEW_TASK_GROUP_EXTENSIONS包裹见 task_group.h。API 概览扩展后的 task_groupTask Bypass Support 并不新增独立方法而是放宽了task_group现有三个成员函数对函数对象返回类型的约束——允许F返回task_handle对象从而向调度器传递下一步执行哪个任务的提示。头文件与类声明如下摘录自 task_bypass_support.rst#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 #include oneapi/tbb/task_group.h namespace oneapi { namespace tbb { class task_group { public: // 仅改变函数 F 的返回类型要求 template typename F task_handle defer(F f); // 仅改变函数 F 的返回类型要求 template typename F task_group_status run_and_wait(const F f); // 仅改变函数 F 的返回类型要求 template typename F void run(F f); }; // class task_group } // namespace tbb } // namespace oneapi三个成员的职责成员签名作用defertask_handle defer(F f)把f包装成task_handle并返回不立即调度由调用方决定后续处理runvoid run(F f)提交f异步执行当f返回task_handle时返回值作为调度提示run_and_waittask_group_status run_and_wait(const F f)提交f并等待其完成同样接受返回task_handle的f其中F必须满足 ISO C 标准 [function.objects] 一节定义的Function Objects函数对象要求。扩展语义Extension文档用专门的 admonition 明确了三条语义规则返回的task_handle是无依赖任务的旁路提示若返回的句柄非空、且其所拥有的任务没有依赖dependencies则该句柄作为接下来可以执行哪个任务的优化提示交给调度器。句柄不得再次显式提交返回的task_handle不能被再次通过task_group::run或其他提交函数显式提交否则行为未定义undefined behavior。句柄必须归属同一 task_group如果返回的句柄由*this之外的其他task_group创建行为同样未定义。此外文档强调被task_handle拥有的延迟任务deferred task其执行既不保证立即发生也不保证由同一线程执行——旁路是提示而非强制调度器有权在满足语义的前提下自主安排。实战示例基于 divide-and-conquer 的 par_for_each文档在 Example 一节通过literalinclude引用了完整可运行示例 task_group_extensions_bypassing.cpp。其核心思路是用task_group 分治divide-and-conquer并行处理序列右半部分用run正常提交左半部分用defer返回为task_handle作为旁路提示从而让当前线程直接续跑左半部分、避免一次 spawn 往返。static constexpr std::size_t serial_threshold 16; #define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 #include oneapi/tbb/task_group.h template typename Iterator, typename Function struct for_task { tbb::task_handle operator()() const { tbb::task_handle next_task; auto size end - begin; if (size serial_threshold) { // 足够小串行执行 for (Iterator it begin; it ! end; it) { f(*it); } } else { // 足够大切分区间 Iterator middle begin size / 2; // 右半部分正常提交 tg.run(for_taskIterator, Function{middle, end, f, tg}); // 左半部分通过 defer 返回作为旁路提示 next_task tg.defer(for_taskIterator, Function{begin, middle, f, tg}); } return next_task; } Iterator begin; Iterator end; Function f; tbb::task_group tg; }; // struct for_task template typename RandomAccessIterator, typename Function void par_for_each(RandomAccessIterator begin, RandomAccessIterator end, Function f) { tbb::task_group tg; // 运行根任务 tg.run_and_wait(for_taskRandomAccessIterator, Function{begin, end, std::move(f), tg}); }示例的main函数构造一个 1000 元素数组通过par_for_each(array, array N, [](std::size_t item) { item 42; })并行赋值随后逐元素校验结果是否为 42。关键写法拆解函数对象必须返回tbb::task_handlefor_task::operator()的返回类型正是tbb::task_handle这是启用旁路的前提。defer与run的分工对左半区间调用defer得到next_task并return对右半区间调用tg.run(...)显式提交。返回句柄会被调度器当作当前线程接下来优先执行的任务。run_and_wait驱动根任务根任务由run_and_wait提交并等待其返回值task_handle同样会被视为旁路提示。串行阈值区间小于serial_threshold16时不再切分转为串行循环避免过度分治。源码级原理从 defer 到 bypass 调度循环1.defer的实现句柄的诞生在 task_group.h 中defer的实现非常简洁templatetypename F d2::task_handle defer(F f) { return prepare_task_handle(std::forwardF(f)); }真正的工作在task_group_base::prepare_task_handletask_group.h中完成templatetypename F d2::task_handle prepare_task_handle(F f) { d1::small_object_allocator alloc{}; using function_task_t d2::function_tasktypename std::decayF::type; d2::task_handle_task* function_task_p alloc.new_objectfunction_task_t(std::forwardF(f), r1::get_thread_reference_vertex(m_wait_vertex), context(), alloc); return d2::task_handle_accessor::construct(function_task_p); }可以推断defer通过 small-object 分配器创建function_task内部task_handle_task子类关联到当前task_group的等待顶点wait vertex与上下文task_group_context再经task_handle_accessor::construct包装成对外暴露的d2::task_handle。注意此时任务尚未被调度——这正是defer推迟命名的由来。2.task_handle的形态移动语义的智能指针task_handle类定义在 _task_handle.hclass task_handle { struct task_handle_task_deleter { void operator()(task_handle_task* p){ p-destroy(); } }; using handle_impl_t std::unique_ptrtask_handle_task, task_handle_task_deleter; handle_impl_t m_handle {nullptr}; public: task_handle() default; task_handle(task_handle) default; task_handle operator(task_handle) default; explicit operator bool() const noexcept { return static_castbool(m_handle); } ... };要点底层是对task_handle_task的独占所有权封装std::unique_ptr因此task_handle仅支持移动语义不可拷贝。explicit operator bool()让调用方可以判断句柄是否为空——返回空句柄等价于无旁路提示。task_handle_task在预览扩展下继承自dynamic_state_task_task_handle.h携带销毁函数指针与分配器保证跨编译单元新旧库版本也能正确析构与释放内存。3. 返回句柄如何进入调度器当run/run_and_wait的函数体返回task_handle后调度器在任务分发循环中处理该返回值。关键证据在 task_dispatcher.h 的主循环// We assume that bypass tasks are from the same task group. context_guard.set_ctx(ed.context); // Inner level evaluates tasks coming from nesting loops and those returned // by just executed tasks (bypassing spawn or enqueue calls). while (t ! nullptr) { ... if (ed.context-is_group_execution_cancelled()) { t t-cancel(ed); } else { t t-execute(ed); } ... t get_critical_task(t, ed, isolation, critical_allowed); }从源码结构看任务执行完毕后execute返回的下一个任务t会直接进入内层 while 循环再次执行而无需经过本地任务队列的 push/pop——这就是旁路发生的物理位置。注释也明确写道内层循环评估来自嵌套循环的任务以及由刚执行完的任务返回的任务旁路了 spawn 或 enqueue 调用。同时run(task_handle)等重载通过task_handle_accessor::release释放句柄中的任务指针task_group.h完成所有权交接。4. 依赖与动态状态旁路的边界条件预览扩展下task_dynamic_state_task_handle.h负责跟踪任务的依赖计数、通知列表与完成转移register_dependency/release_dependency/has_dependencies管理依赖计数complete_and_try_get_successor/cancel_and_try_get_successor在任务完成/取消时尝试取出后继任务通知节点分两类notify_successor_node唤醒后继任务与notify_waiter_node唤醒等待者。其中notify_waiter_node::notify_common的注释明确如果通知列表中有等待者则不允许从列表通知中旁路返回{nullptr, false}禁用旁路。结合internal_run_and_wait(d2::task_handle)的实现task_group.h若任务尚有依赖且释放依赖后仍未就绪则走d1::wait等待否则直接execute_and_wait。这印证了文档返回的句柄只对无依赖任务构成旁路提示的语义——有依赖的任务必须先满足依赖关系无法被立即旁路。测试验证约束与语义的守护oneTBB 的测试套件对 Task Bypass 支持有直接覆盖核心用例位于 test_task_group.cppTask handle for scheduler bypasstest_task_group.cpp在run提交的函数体内return tg.defer(...)随后tg.wait()校验被旁路的 lambda 确实被执行同时验证返回空句柄tbb::task_handle{}的合法性。Task handle for scheduler bypass via run_and_waittest_task_group.cpp同一语义在run_and_wait路径下的验证。Empty task_handle cannot be scheduledtest_task_group.cpp提交空句柄会触发 Attempt to schedule empty task_handle 断言——对应源码中__TBB_ASSERT(h ! nullptr, ...)的防御性检查。task_handle cannot be scheduled into different task_grouptest_task_group.cpp把tg创建的句柄提交给另一个tg1会触发 Attempt to schedule task_handle into different task_group 断言——与文档句柄必须归属同一 task_group的约束一一对应。此外还有大量基于defer的进阶用例动态依赖、reduction、完成转移等例如 test_task_group.cpp 中用defer构造左右叶子任务与 combine 任务的依赖图。这些用例连同test/tbb/test_task_group.cpp顶部由 config.h 统一启用的__TBB_PREVIEW_TASK_GROUP_EXTENSIONS表明Task Bypass 不仅是一纸文档其语义被显式测试守护。注意事项与适用前提预览特性该扩展依赖TBB_PREVIEW_TASK_GROUP_EXTENSIONS宏属于预览 API接口在未来版本中可能调整生产使用前应关注 oneTBB 的版本演进。返回值即所有权转移一旦函数返回task_handle该句柄的所有权即移交调度器不可再次提交也不可跨task_group使用。旁路是提示不是指令被旁路任务的执行时机与执行线程均由调度器决定不要在返回句柄后假设其立刻在当前线程运行。与动态依赖的交互仅无依赖任务可被直接旁路带依赖的任务需经由task_dynamic_state的依赖计数与通知机制调度。性能收益的前提Task Scheduler Bypass 的收益来自减少 deque 操作与偷取导致的局部性损失Task_Scheduler_Bypass.rst适合当前线程应立刻续跑下一任务的分治类工作负载在任务极小、调度开销已不显著的场景下收益有限应结合 profiling 决策。小结Task Bypass Support 通过放宽task_group::run、defer、run_and_wait对函数对象返回类型的约束让开发者用返回task_handle的方式向调度器传递下一个执行谁的提示从而绕开 spawn 路径中的队列 push/pop 与偷取风险。其核心链条清晰可循文档与示例task_bypass_support.rst、task_group_extensions_bypassing.cpp对外 API 与句柄实现task_group.h、_task_handle.h调度器旁路循环task_dispatcher.h语义守护测试test_task_group.cpp。对 mold 仓库的读者而言这一内嵌 oneTBB 扩展为链接器内部可并行的分治逻辑如符号排序、压缩、去重等阶段的递归切分提供了降低调度开销的实用手段——启用宏、按约束返回句柄即可在既有task_group代码上以最小改动获得 bypass 收益。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考