ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

oneTBB Flow Graph 节点优先级(node_priorities)深入解析:用关键路径优先调度提升并行图性能

oneTBB Flow Graph 节点优先级(node_priorities)深入解析:用关键路径优先调度提升并行图性能 oneTBB Flow Graph 节点优先级node_priorities深入解析用关键路径优先调度提升并行图性能【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold本文基于当前仓库内嵌的 oneTBBIntel oneAPI Threading Building Blocks官方规范文档 node_priorities.rst 展开完整讲解 Flow Graph 节点优先级机制的公开 API、调度语义与源码级实现原理并给出可直接编译运行的关键路径优先实战示例。读完本文你将掌握node_priority_t的取值规则、四类支持优先级的节点构造方式、优先级任务在 oneTBB 调度器中的实际流转路径以及如何用优先级引导线程优先执行关键路径、缩短整图执行时间。上图为官方规范文档配图展示了一个带关键路径的依赖流图节点bs广播、f1/f2/f3并行分支、fe汇合当f2耗时显著长于f1、f3时bs → f2 → fe构成整图的关键路径优先调度f2可以显著缩短整体执行时间。1. 特性概述为什么需要节点优先级Flow Graph 是一种有向依赖图图中每个功能节点function 型节点在被触发后会产生对应的执行任务task。在默认情况下这些任务以大致公平的方式被线程调度执行顺序具有非确定性两个并发线程可能先并行执行耗时短的节点把耗时长的关键路径节点晾在一边导致整图总执行时间被拉长。节点优先级特性[flow_graph.node_priorities]正是为解决这一问题而设计的在构造功能节点时传入一个相对优先级数值引导执行该图的线程优先选择高优先级节点的任务。它不改变图的依赖关系只改变任务被线程拣选时的先后次序属于软性引导而非抢占式硬调度详见第 5 节实现原理。2. 核心 APInode_priority_t 与 no_priority优先级类型与默认常量定义在oneapi::tbb::flow命名空间中官方规范原文如下namespace oneapi { namespace tbb { namespace flow { typedef unsigned int node_priority_t; const node_priority_t no_priority node_priority_t(0); } // namespace flow } // namespace tbb } // namespace oneapi要点node_priority_t是unsigned int的别名即优先级是一个非负整数no_priority等于0是各节点构造器优先级参数的默认值表示对该节点关闭优先级传入的数值越大优先级越高没有内置的上限数值的相对大小即相对优先级由于是无符号整数不存在负优先级——需要比无优先级更低的调度优先级时只需保持no_priority0即可高优先级节点自然会被优先拣选。在仓库实现中该类型与常量定义于 _flow_graph_impl.htypedef unsigned int node_priority_t; __TBB_GLOBAL_VAR constexpr node_priority_t no_priority node_priority_t(0);并通过 flow_graph.h 的using声明公开给oneapi::tbb::flow命名空间。3. 优先级语义线程如何选择任务官方规范对调度语义的描述可以归纳为三条规则同一图内已指定优先级的节点任务相对于低优先级或未指定优先级的节点任务具有优先权线程在寻找可执行任务时会从图中当前可执行的任务集合里选择优先级最高的那个no_priority相当于关闭优先级即该节点任务退化为普通任务参与调度。需要强调的是这种引导是**尽力而为best-effort**的它影响线程拣选任务的倾向但不保证任何确定性顺序也不做任务抢占。文档明确说明这是为了guiding threads that execute the graph to prefer nodes with higher priority因此不应把优先级机制当作严格的分级调度器使用。4. 支持优先级的节点类型与构造器签名官方规范指出以下四类功能节点支持在构造时传入node_priority_t参数节点类型说明构造器中的优先级参数位置function_node单输入单输出函数节点传入node_priority_t a_priority默认no_prioritymultifunction_node单输入多输出函数节点同上async_node支持异步外部活动的节点同上continue_node依赖广播/前驱完成信号的节点同上以 flow_graph.h 中function_node的构造器声明为例L905-L928template typename Body, typename Policy /* 默认策略 */ function_node( graph g, size_t concurrency, Body body, Policy Policy(), node_priority_t a_priority no_priority ) function_node( graph g, size_t concurrency, Body body, node_priority_t a_priority )multifunction_nodeL998-L1023、continue_nodeL1123-L1182与async_nodeL2873-L2902的构造器同样以node_priority_t a_priority no_priority作为可选尾参类模板推导指引deduction guide在 _flow_graph_nodes_deduction.h 中同样默认node_priority_t no_priority。5. 源码级实现原理本节结合仓库内实际实现说明优先级从节点构造参数到线程任务拣选的完整流转链路。5.1 任务承载优先级graph_task.priority每个由节点产生的执行任务都是graph_task对象它在构造时接收节点传入的优先级并保存在priority成员中_flow_graph_impl.hclass graph_task : public d1::task { graph_task(graph g, d1::small_object_allocator allocator, node_priority_t node_priority no_priority); ... node_priority_t priority; // 任务携带的优先级 };节点的输入基类function_input_base会把构造时传入的a_priority存入my_priority_flow_graph_node_impl.h并在创建 body 执行任务与转发任务时把该值传给任务构造器_flow_graph_node_impl.hinline graph_task* create_forward_task() { ... graph_task* t allocator.new_objecttask_type( graph_reference(), allocator, *this, my_priority ); return t; }也就是说节点的优先级会沿用到它产生的每一个任务包括 body 执行任务与消息转发任务。5.2 非抢占式优先队列调度prioritize_task 与 priority_task_selector带优先级的任务在入图调度时会走一条特殊路径_flow_graph_impl.hinline graph_task* prioritize_task(graph g, graph_task gt) { if( no_priority gt.priority ) return gt; // 无优先级直接按普通任务提交 //! Non-preemptive priority pattern. The original task is submitted as a work item //! to the priority queue, and a new critical task is created to take and execute //! a work item with the highest known priority. d1::small_object_allocator allocator; d1::task* critical_task allocator.new_objectpriority_task_selector(g.my_priority_queue, allocator); g.my_priority_queue.push(gt); // 原任务进入并发优先队列 submit( *critical_task, *g.my_task_arena, *g.my_context, /*as_critical*/true ); return nullptr; }关键设计源码注释中明确称为Non-preemptive priority pattern即非抢占式优先级模式每个graph对象持有一个并发优先队列graph_task_priority_queue_t其底层是tbb::concurrent_priority_queuegraph_task*, graph_task_comparator_flow_graph_impl.h比较器按priority数值降序取最大者struct graph_task_comparator { bool operator()(const graph_task* left, const graph_task* right) { return left-priority right-priority; } };带优先级的原任务被压入该队列同时创建一个哨兵任务priority_task_selector_flow_graph_impl.h它以 critical task 形式提交到图的任务 arena 中执行时从中弹出当前已知最高优先级的任务来运行。这就是线程选择优先级最高的可执行任务这一语义的落地点无优先级no_priority的任务完全不走此队列直接按普通任务提交避免额外开销。可见优先级机制并没有插队打断正在运行的任务而是通过一个高优先级哨兵从队列中优先取出关键任务属于协作式/非抢占式调度符合文档中引导线程优先选择的描述。5.3 后继缓存中的转发顺序优化除了任务调度层面优先级还影响消息转发顺序。successor_cache::register_successor在注册后继节点时会把有优先级的后继插入链表头部、无优先级的插入尾部_flow_graph_cache_impl.hcontinue_msg特化见 L336-L346void register_successor( successor_type r ) { ... if( r.priority() ! no_priority ) my_successors.push_front( r ); // 高优先级后继优先获得转发 else my_successors.push_back( r ); }这意味着广播/转发时高优先级节点更早收到消息、更早产生任务与任务级优先队列形成配合。源码中留有注释// TODO revamp: introduce heapified collection of successors for strict priorities即当前实现是通过链表头插实现的最佳努力顺序而非严格的堆化排序——这一点可作为理解其尽力而为语义的佐证。5.4 测试验证仓库的专门测试 test_flow_graph_priorities.cpp 覆盖了优先级语义。例如PriorityNodesTakePrecedence用例L63-L119构造 100 个节点其中中间 1/3 区间start_index到end_index的节点以node_priority_t(index)设置递增优先级其余节点保持no_priority随后验证高优先级节点priority ! no_priority产生的任务确实先于普通节点被拾取多个高优先级任务之间按优先级数值的相对大小被依次执行。测试同时覆盖function_node、multifunction_node、continue_node_flow_graph_priorities.cpp 中的 node_creator_t与async_node与规范文档声明的四类节点一一对应。6. 实战示例关键路径优先官方示例源码位于 node_priorities.cpp其完整内容如下#include iostream #include cmath #include oneapi/tbb/tick_count.h #include oneapi/tbb/global_control.h #include oneapi/tbb/flow_graph.h void spin_for( double delta_seconds ) { oneapi::tbb::tick_count start oneapi::tbb::tick_count::now(); while( (oneapi::tbb::tick_count::now() - start).seconds() delta_seconds ) ; } static const double unit_of_time 0.1; struct Body { unsigned factor; Body( unsigned times ) : factor( times ) {} void operator()( const oneapi::tbb::flow::continue_msg ) { // body execution takes factor units of time spin_for( factor * unit_of_time ); } }; int main() { using namespace oneapi::tbb::flow; const int max_threads 2; oneapi::tbb::global_control control(oneapi::tbb::global_control::max_allowed_parallelism, max_threads); graph g; broadcast_nodecontinue_msg bs(g); continue_nodecontinue_msg f1(g, Body(5)); // f2 is a heavy one and takes the most execution time as compared to the other nodes in the // graph. Therefore, let the graph start this node as soon as possible by prioritizing it over // the other nodes. continue_nodecontinue_msg f2(g, Body(10), node_priority_t(1)); continue_nodecontinue_msg f3(g, Body(5)); continue_nodecontinue_msg fe(g, Body(7)); make_edge( bs, f1 ); make_edge( bs, f2 ); make_edge( bs, f3 ); make_edge( f1, fe ); make_edge( f2, fe ); make_edge( f3, fe ); oneapi::tbb::tick_count start oneapi::tbb::tick_count::now(); bs.try_put( continue_msg() ); g.wait_for_all(); double elapsed std::floor((oneapi::tbb::tick_count::now() - start).seconds() / unit_of_time); std::cout Elapsed approximately elapsed units of time std::endl; return 0; }逐段解读节点与耗时f1、f3各耗时 5 个时间单位f2耗时 10 个单位fe耗时 7 个单位Body(factor)中的 factor ×unit_of_time其中unit_of_time 0.1秒图结构bsbroadcast_node一次把continue_msg广播给f1、f2、f3三个分支三者的输出汇聚到fecontinue_node 需等待全部前驱完成后才执行关键路径分析bs → f2 → fe是耗时最长的一条依赖链10 7 17 个单位构成整图的 critical path而bs → f1/f3 → fe只要 12 个单位不加优先级的问题两个线程可能先并行执行f1、f3各 5 个单位fe要等三个分支全部完成于是f2启动被推迟总时间被拉长到接近 10 7 5 等更长区间加优先级的效果给f2传node_priority_t(1)后线程从bs广播后产生的三个任务中优先拣选f2的任务使关键路径尽早开工fe也能更早收到f2的完成信号整体执行时间显著缩短测量方式用oneapi::tbb::tick_count计时结果折算为时间单位输出global_control把最大并行度限制为 2模拟双线程场景。运行方式以本项目仓库为上下文需预先构建好 oneTBB# 假设已编译 oneTBB 并设置了 include/lib 路径 c -O2 -stdc17 node_priorities.cpp -I tbb_include -L tbb_lib -ltbb -o node_priorities ./node_priorities7. 使用建议与注意事项优先级的取值策略node_priority_t只定义相对大小关系无内置上限。示例中用node_priority_t(1)表示略高于默认复杂场景可用更大数值区分多个优先级层次如测试中直接使用 0~99 的索引值——注意测试与示例都依赖数值越大越优先的约定只对功能型节点有效目前仅function_node、multifunction_node、async_node、continue_node四类节点支持该参数graph、broadcast_node、buffer_node等基础设施节点不参与不要与priority_queue_node混淆priority_queue_node是按消息优先级排序的缓冲节点见 priority_queue_node_cls.rst它解决的是消息排队顺序而本文的节点优先级解决的是任务被线程拣选的顺序两者机制不同非抢占、非确定性优先级是引导性的不保证严格的分级顺序也不打断正在运行的任务见 5.2 节源码注释对强实时性要求不应依赖该机制默认关闭、零额外开销不传该参数默认no_priority时任务直接走普通提交路径prioritize_task 中no_priority gt.priority的提前返回因此只为确有需要的节点开启即可适用场景图中存在明显的不均衡负载或关键路径、且线程数少于可并行分支数时收益最明显若各分支耗时接近优先级带来的收益有限。8. 相关参考路径规范文档node_priorities.rst官方示例node_priorities.cpp公开 API 头文件flow_graph.h任务优先队列与调度实现_flow_graph_impl.h后继转发顺序实现_flow_graph_cache_impl.h节点优先级存储与任务创建_flow_graph_node_impl.h优先级专项测试test_flow_graph_priorities.cpp相关节点文档function_node、continue_node、async_node【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表