辅助函数:用 `precedes`/`follows` 一行代码连接整个流图`)
oneTBB 流图节点集node_set辅助函数用precedes/follows一行代码连接整个流图【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读本篇文章以 oneTBBoneAPI Threading Building Blocks仓库中的实验性 RFC 文档为核心系统讲解其流图flow graph扩展make_node_set、make_edges、precedes与follows四个辅助函数以及配套的节点构造函数重载与 C17 类模板实参推导CTAD指引。它们把逐条调用make_edge连边的冗长写法压缩为一行构造式声明特别适合构建扇出一节点多后继、扇入多前驱汇聚到一节点以及multifunction_node/join_node等多端口节点的图结构。读完本文你将掌握这套预览 API 的全部用法、底层实现机制、各节点类型的端口连接规则以及它的已知限制与开放设计问题。一、动机为何需要节点集扩展在生产版流图 API 中创建多条边需要逐条调用make_edge代码冗长且可读性差。以input节点连接三个后继为例传统写法是broadcast_nodeint input(g); function_node doubler(g, unlimited, [](const int v) { return 2 * v; }); function_node squarer(g, unlimited, [](const int v) { return v * v; }); function_node cuber(g, unlimited, [](const int v) { return v * v * v; }); make_edge(input, doubler); make_edge(input, squarer); make_edge(input, cuber);利用实验扩展可以改写为单个构造函数调用一次性声明节点并完成连边function_node doubler(g, unlimited, [](const int v) { return 2 * v; }); function_node squarer(g, unlimited, [](const int v) { return v * v; }); function_node cuber(g, unlimited, [](const int v) { return v * v * v; }); broadcast_node input(precedes(doubler, squarer, cuber));两段代码产生的图结构完全相同但后者把创建节点 声明依赖关系合并成一步意图一目了然。这套实验扩展由四个部分组成make_node_set函数模板把多个节点收集为一个节点集make_edges函数模板在单个节点与节点集之间批量建边部分而非全部流图节点类型的新增构造函数接受precedes/follows的返回值以替代graph参数follows与precedes函数模板为节点集附加顺序标记。二、启用实验特性该特性属于预览preview功能。在使用之前需要在包含oneapi/tbb/flow_graph.h之前定义宏#define TBB_PREVIEW_FLOW_GRAPH_FEATURES 1 #include oneapi/tbb/flow_graph.h从源码看宏的传递链路位于 lib/config 相关头文件当TBB_PREVIEW_FLOW_GRAPH_FEATURES未定义时默认取__TBB_CPF_BUILD的值随后__TBB_PREVIEW_FLOW_GRAPH_NODE_SET直接与TBB_PREVIEW_FLOW_GRAPH_FEATURES绑定而真正控制本特性代码的是__TBB_PREVIEW_FLOW_GRAPH_NODE_SET宏在 _flow_graph_node_set_impl.h 中整体包裹。因此只需打开TBB_PREVIEW_FLOW_GRAPH_FEATURES即可同时开启节点集特性。仓库自带的示例代码均采用这一开关例如 make_edges_function_example.cpp 与 helpers_for_expressing_graphs_preview_api_example.cpp。三、node_set与make_node_set3.1node_set的形态在实验文档中node_set是make_node_set返回类型的仅作说明exposition-only名称。当前实现里它是一个模板结构体namespace tbb { namespace flow { namespace order { struct undefined {}; struct following {}; struct preceding {}; } templatetypename Order, typename... Nodes struct node_set { typedef Order order_type; std::tupleNodes... nodes; node_set(Nodes... ns) : nodes(ns...) {} template typename... Nodes2 node_set(const node_setorder::undefined, Nodes2... set) : nodes(set.nodes) {} graph graph_reference() const { return get_graph_helper::get(std::get0(nodes)); } }; template typename Node, typename... Nodes node_setorder::undefined, Node, Nodes... make_node_set(Node first_node, Nodes... nodes) { return node_setorder::undefined, Node, Nodes...(first_node, nodes...); } } }要点如下node_set以std::tupleNodes...保存各节点的引用第一个模板参数Order是顺序标记取值来自order::undefined、order::following、order::preceding三个空结构体。make_node_set构造出的集合顺序标记为order::undefinedprecedes/follows则会在此基础上派生出带order::preceding/order::following标记的新集合。graph_reference()返回集合中第一个节点所属的图对象内部通过get_graph_helper获得若节点是graph_node派生类直接取其my_graph成员否则回退到object.graph_reference()见 _flow_graph_node_set_impl.h。3.2 使用约束一个node_set可以传给make_edges、make_edges_in_order、follows和precedes使用。理论上集合内所有节点应来自同一个图但实现并不强制检查混入不同图的节点属于未定义行为UB。由于graph_reference()只取第一个节点跨图混用时图引用将无法正确还原这也正是同一图假设在实现层面的体现。四、follows与precedes语义是相对的4.1 函数签名// node_set 是 make_node_set 返回类型的 exposition-only 名称 template typename NodeType, typename... NodeTypes /*unspecified*/ follows( node_setNodeType, NodeTypes... set ); // [1] template typename NodeType, typename... NodeTypes /*unspecified*/ follows( NodeType node, NodeTypes... nodes ); // [2] template typename NodeType, typename... NodeTypes /*unspecified*/ precedes( node_setNodeType, NodeTypes... set ); // [3] template typename NodeType, typename... NodeTypes /*unspecified*/ precedes( NodeType node, NodeTypes... nodes ); // [4]重载 [1]/[3] 接受一个node_set为其加上顺序标记后返回新集合重载 [2]/[4] 直接接受一串节点返回带标记的新集合。例如auto handlers make_node_set(n1, n2, n3); broadcast_nodeint n0(precedes(handlers));handlers先以order::undefined构造precedes(handlers)返回order::preceding的新集合等价于直接写broadcast_nodeint n0(precedes(n1, n2, n3));4.2 命名易混淆点preceding集合装的是后继RFC 特别澄清了语义node_set的顺序标记并非描述集合自身的固有属性而是描述待构造节点 N 相对集合中节点的位置order::following集合中的节点会成为N 的前驱因为 N 跟在集合节点之后order::preceding集合中的节点会成为N 的后继因为 N 排在集合节点之前。也就是说一个preceding集合里装的实际是将来的后继节点following集合里装的则是将来的前驱节点。RFC 以precedes的实现为例说明命名由来——它的形参被命名为successorstemplatetypename FirstSuccessor, typename... Successors node_setorder::preceding, FirstSuccessor, Successors... precedes(FirstSuccessor first_successor, Successors... successors) { return node_setorder::preceding, FirstSuccessor, Successors...(first_successor, successors...); }在真实实现中precedes与follows还带有编译期约束follows要求集合内所有节点具有output_type且是sender发送者precedes要求所有节点具有input_type且是receiver接收者不满足会触发static_assert见 _flow_graph_node_set_impl.h。这也印证了following集合必须能向前驱发消息、preceding集合必须能接收消息的角色划分。五、make_edges批量建边make_edges在单个节点与节点集之间批量创建边参数的左右位置决定了前驱/后继关系左参数中的节点是右参数中的节点的前驱。templatetypename NodeType, typename OrderFlagType, typename... Args void make_edges(const node_setOrderFlagType, Args... s, NodeType node); template typename NodeType, typename OrderFlagType, typename... Args void make_edges(NodeType node, const node_setOrderFlagType, Args... s);make_edges(node, node_set)node 成为集合中每个节点的前驱单对多扇出make_edges(node_set, node)集合中每个节点成为 node 的前驱多对一扇入。完整示例来自 make_edges_function_example.cpp#define TBB_PREVIEW_FLOW_GRAPH_FEATURES 1 #include oneapi/tbb/flow_graph.h int main() { using namespace oneapi::tbb::flow; graph g; broadcast_nodeint input(g); function_node doubler(g, unlimited, [](const int i) { return 2 * i; }); function_node squarer(g, unlimited, [](const int i) { return i * i; }); function_node cuber(g, unlimited, [](const int i) { return i * i * i; }); buffer_nodeint buffer(g); auto handlers make_node_set(doubler, squarer, cuber); make_edges(input, handlers); // input 扇出到三个 function_node make_edges(handlers, buffer); // 三个 function_node 汇聚到 buffer for (int i 1; i 10; i) { input.try_put(i); } g.wait_for_all(); }5.1 底层实现make_edges_helpermake_edges的实现在 _flow_graph_node_set_impl.h 中借助递归模板make_edges_helper展开connect_predecessors对集合中每个节点调用make_edge(std::getI(predecessors), successor_selectorI::get(node))即集合节点 → 目标节点connect_successors对集合中每个节点调用make_edge(predecessor_selectorI::get(node), std::getI(successors))即目标节点 → 集合节点。其中successor_selector/predecessor_selector是关键适配层若节点具有多端口存在input_ports_type/output_ports_type则自动改用input_portI(node)/output_portI(node)取第 I 个端口否则退化为节点本身predecessor_selector还特判了async_node直接返回节点见 _flow_graph_node_set_impl.h。这就是单端口节点直接用、多端口节点按索引选端口的实现细节。5.2make_edges_in_order实验中另有一个make_edges_in_order函数模板总是把node_set放在第一个参数且不接受order::undefined集合template typename NodeType, typename... Nodes void make_edges_in_order(const node_setorder::following, Nodes... ns, NodeType node); template typename NodeType, typename... Nodes void make_edges_in_order(const node_setorder::preceding, Nodes... ns, NodeType node);它当前用于简化节点构造函数的实现并未作为面向用户的函数记录在文档中。其内部分别转调make_edges(ns, node)与make_edges(node, ns)依赖following/preceding标记决定连边方向。注意make_edges与make_edges_in_order面向单一输入/单一输出端口的节点。多入多出节点属于特例见下一节。六、新增构造函数用节点集替代graph参数大多数流图节点新增了接受order::following或order::preceding集合的构造函数重载。构造时被构造节点的图引用通过node_set::graph_reference()从集合中第一个节点取得——再次强调这要求集合内所有节点属于同一个图。根据端口形状节点被分为四类规则各不相同。6.1 单输入单输出节点following/preceding均可这类节点全部支持两种顺序标记构造函数汇总如下// continue_node template typename Body, typename... Args continue_node( const node_setArgs... nodes, Body body, Policy p Policy(), node_priority_t a_priority no_priority ); template typename Body, typename... Args continue_node( const node_setArgs... nodes, Body body, node_priority_t a_priority); template typename Body, typename... Args continue_node( const node_setArgs... nodes, int number_of_predecessors, Body body, Policy p Policy(), node_priority_t a_priority no_priority ); template typename Body, typename... Args continue_node( const node_setArgs... nodes, int number_of_predecessors, Body body, node_priority_t a_priority ); // function_node template typename Body, typename... Args function_node( const node_setArgs... nodes, size_t concurrency, Body body, Policy p Policy(), node_priority_t a_priority no_priority ); template typename Body, typename... Args function_node( const node_setArgs... nodes, size_t concurrency, Body body, node_priority_t a_priority ); // async_node template typename Body, typename... Args async_node( const node_setArgs... nodes, size_t concurrency, Body body, Policy Policy(), node_priority_t a_priority no_priority ); template typename Body, typename... Args async_node(const node_setArgs... nodes, size_t concurrency, Body body, node_priority_t a_priority); // overwrite_node template typename... Args overwrite_node(const node_setArgs... nodes); // write_once_node template typename... Args write_once_node(const node_setArgs... nodes); // buffer_node template typename... Args buffer_node(const node_setArgs... nodes); // queue_node template typename... Args queue_node( const node_setArgs... nodes); // priority_queue_node template typename... Args priority_queue_node(const node_setArgs... nodes, const Compare comp Compare()); // sequencer_node template typename Sequencer, typename... Args sequencer_node( const node_setArgs... nodes, const Sequencer s); // limiter_node template typename... Args limiter_node(const node_setArgs... nodes, size_t threshold); // broadcast_node template typename... Args broadcast_node(const node_setArgs... nodes);所有重载都保留原有的Policy如queueing/rejecting/lightweight、node_priority_t、concurrency如unlimited/serial等参数只是把第一个graph参数替换为节点集。仓库一致性测试覆盖了这些重载的多种组合例如 conformance_function_node.cpp 中function_node f5(follows(pred_f), unlimited, body)与function_node f9(precedes(succ_f), unlimited, body)等写法以及 conformance_continue_node.cpp 中continue_node c9(follows(b), body)等写法。6.2 无输入端口节点input_nodeinput_node没有输入端口因此只能接受order::preceding集合// input_node // NOTE: an input_node has no input port itself, so cannot receive an order::following node_set template typename Body, typename... Successors input_node( const node_setorder::preceding, Successors... successors, Body body );对应测试见 conformance_input_node.cpp 中的input_node s5(precedes(bc), lambda)。6.3 单输入多输出节点multifunction_node与split_node多输出端口节点的特例规则收到order::following集合与普通节点一致集合内所有节点成为新节点的前驱各连一条边到其唯一输入端口收到order::preceding集合由于无法判断哪个输出端口连哪个后继采用简单规则——集合大小必须恰好等于输出端口数构造时按索引一一对应连边第 i 个输出端口连集合中第 i 个节点。// multifunction_node // Has a single input port and multiple output ports template typename Body, typename... Args multifunction_node(const node_setArgs... nodes, size_t concurrency, Body body, Policy p Policy(), node_priority_t a_priority no_priority); template typename Body, typename... Args multifunction_node(const node_setArgs... nodes, size_t concurrency, Body body, node_priority_t a_priority); // split_node // Has a single input port and multiple output ports template typename... Args split_node(const node_setArgs... nodes);RFC 给出的完整可运行示例tbb::flow::precedes(n2, n3)会让双输出multifunction_node的output_port0连到n2、output_port1连到n3int main() { tbb::flow::graph g; tbb::flow::function_nodeint,int n2{ g, 1, [](int msg) { std::printf(2:%d\n, msg); return msg; } }; tbb::flow::function_nodeint,int n3(g, 1, [](int msg) { std::printf(3:%d\n, msg); return msg; }); using mfn_t tbb::flow::multifunction_nodeint, std::tupleint, int ; mfn_t n1(tbb::flow::precedes(n2, n3), 1, [](int msg, mfn_t::output_ports_type p) { std::printf(1:%d\n, msg); std::get0(p).try_put(msg * 2); std::get1(p).try_put(msg * 4); }); n1.try_put(100); g.wait_for_all(); return 0; }运行结果一次实测输出表明100进入n1后100*2被送到集合中第一个节点n2100*4被送到第二个节点n31:100 3:400 2:200输出顺序因并发调度而可能变化但数值对应关系固定。6.4 单输出多输入节点join_node与indexer_node多输入端口节点join_node、indexer_node是另一类特例收到order::preceding集合与普通节点一致集合内所有节点成为新节点的后继各连一条边到其唯一输出端口收到order::following集合集合大小必须恰好等于输入端口数构造时按索引连边——集合中第 i 个节点连到第 i 个输入端口。// join_node // Has multiple input ports and a single output port template typename... Args join_node(const node_setArgs... nodes, Policy Policy()); // indexer_node // Has multiple input ports and a single output port template typename... Args indexer_node(const node_setArgs... nodes);典型用法可同时出现在一个图中来自 helpers_for_expressing_graphs_preview_api_example.cppfunction_node doubler(g, unlimited, [](const int v) { return 2 * v; }); function_node squarer(g, unlimited, [](const int v) { return v * v; }); function_node cuber(g, unlimited, [](const int v) { return v * v * v; }); auto handlers make_node_set(doubler, squarer, cuber); broadcast_node input(precedes(handlers)); // input 扇出到三个处理器 join_node join(follows(handlers)); // 三个处理器按索引汇聚到 join int sum 0; function_node summer(follows(join), serial, { int sub_sum std::get0(v) std::get1(v) std::get2(v); sum sub_sum; return sub_sum; });这里join_node join(follows(handlers))的following集合恰好包含 3 个节点与join_nodeint,int,intjoin_nodestd::tupleint,int,int的 CTAD 结果见下文推导指引的 3 个输入端口按序对应。6.5 新增构造函数的注意事项无论普通还是特例情形构造函数一次只接收前驱集合或后继集合不能同时传入两者边的前驱/后继关系由集合的顺序标记决定following→ 集合为前驱preceding→ 集合为后继这与make_edges靠参数左右位置推断关系的方式不同当前不支持composite_node。七、显式推导指引让 CTAD 从节点集推导模板实参流图节点自 C17 起支持类模板实参推导CTAD本实验特性借node_set携带的额外信息增加了若干显式推导指引。7.1 单输入单输出类型节点对于具有单一公共输入/输出类型的节点overwrite_node、write_once_node、broadcast_node、buffer_node、queue_node、priority_queue_node、limiter_nodegraph对象本身不携带类型信息而node_set中节点的输入/输出类型可以用于推导order::following集合 → 取Node::output_type集合是前驱看它们输出什么order::preceding集合 → 取Node::input_type集合是后继看它们接收什么。实现用decide_on_set特征类完成选择template typename NodeSet struct decide_on_set; template typename Node, typename... Nodes struct decide_on_setnode_setorder::following, Node, Nodes... { using type typename Node::output_type; }; template typename Node, typename... Nodes struct decide_on_setnode_setorder::preceding, Node, Nodes... { using type typename Node::input_type; }; template typename NodeSet using decide_on_set_t typename decide_on_setstd::decay_tNodeSet::type; template typename NodeSet overwrite_node(const NodeSet) -overwrite_nodedecide_on_set_tNodeSet; template typename NodeSet write_once_node(const NodeSet) -write_once_nodedecide_on_set_tNodeSet; template typename NodeSet buffer_node(const NodeSet) -buffer_nodedecide_on_set_tNodeSet; template typename NodeSet queue_node(const NodeSet) -queue_nodedecide_on_set_tNodeSet; template typename NodeSet, typename Compare priority_queue_node(const NodeSet, const Compare) -priority_queue_nodedecide_on_set_tNodeSet, Compare; template typename NodeSet priority_queue_node(const NodeSet) -priority_queue_nodedecide_on_set_tNodeSet, std::lessdecide_on_set_tNodeSet; template typename NodeSet limiter_node(const NodeSet, size_t) -limiter_nodedecide_on_set_tNodeSet; template typename NodeSet broadcast_node(const NodeSet) -broadcast_nodedecide_on_set_tNodeSet;7.2 多输入节点join_node与indexer_node提供order::following集合时N 个节点对应 N 个输入端口join_node的元组类型由各前驱的output_type推导indexer_node同理// join_node template typename Policy, typename... Predecessors join_node(const node_setorder::following, Predecessors..., Policy) -join_nodestd::tupletypename Predecessors::output_type..., Policy; template typename... Predecessors join_node(const node_setorder::following, Predecessors...) -join_nodestd::tupletypename Predecessors::output_type..., queueing; // indexer_node template typename... Predecessors indexer_node(const node_setorder::following, Predecessors...) -indexer_nodetypename Predecessors::output_type...;提供order::preceding集合时集合内节点都接到唯一输出端口join_node的元组类型直接从集合第一个节点的input_type推导indexer_node目前没有这一情形的显式推导指引// join_node template typename Policy, typename Successor, typename... Successors join_node(const node_setorder::preceding, Successor, Successors..., Policy) -join_nodetypename Successor::input_type, Policy; template typename Successor, typename... Successors join_node(const node_setorder::preceding, Successor, Successors...) -join_nodetypename Successor::input_type, queueing;7.3 多输出节点split_nodesplit_node是唯一不靠 callable 推导的多输出节点现在可借node_set推导模板实参template typename Predecessor, typename... Predecessors split_node(const node_setorder::following, Predecessor, Predecessors...) -split_nodetypename Predecessor::output_type; template typename... Successors split_node(const node_setorder::preceding, Successors...) -split_nodestd::tupletypename Successors::input_type...;7.4 推导指引覆盖范围说明带 callable 的节点function_node、continue_node、sequencer_node的显式推导指引基于 callable 本身与node_set支持无关本文不再展开multifunction_node与async_node目前没有显式推导指引其支持能否加入还需进一步研究一致性测试对推导结果有直接验证例如 conformance_join_node.cpp 中join_node j1(follows(b1, b2, b3))、join_node j3(precedes(b4))均依赖上述指引完成类型推导。八、开放问题与退出标准RFC 文档明确列出了本实验特性进入正式规范前需要解决的开放问题与退出标准。8.1 开放问题构造函数一次只能设置前驱或后继、不能同时设置两者这一限制是否可接受多端口节点的特例规则按索引一一连边、集合大小必须等于端口数是否符合预期node_set应该正式定义在规范中还是继续作为 exposition-only 类型若正式定义order标记的命名preceding集合装的却是后继是否会造成混淆node_set支持是否应扩展到composite_nodeindexer_node的输出端口能否借助node_set为multifunction_node和async_node增加显式推导指引8.2 退出标准获得足够多的用户反馈以敲定上述开放问题oneTBB 规范文档完成更新并被接受。九、更多参考与延伸阅读若要在实际工程中使用这套 API可继续查阅仓库内以下资源RFC 原文本文主体third-party/tbb/rfcs/experimental/flow_graph_node_sets/README.md含两张示意图与全部示例代码核心实现third-party/tbb/include/oneapi/tbb/detail/_flow_graph_node_set_impl.hnode_set、precedes/follows、make_edges及端口选择器的完整源码功能开关定义third-party/tbb/include/oneapi/tbb/detail/_config.h官方参考文档helpers_for_expressing_graphs.rst、make_node_set_function.rst、follows_and_precedes_functions.rst、make_edges_function.rst、constructors_for_nodes.rst可运行示例make_edges_function_example.cpp、helpers_for_expressing_graphs_preview_api_example.cpp一致性测试test/conformance/目录下的 conformance_function_node.cpp、conformance_continue_node.cpp、conformance_join_node.cpp、conformance_input_node.cpp 等以及通用测试头 test_follows_and_precedes_api.h结语node_set实验扩展的价值在于把节点创建与依赖关系声明两个步骤合而为一precedes/follows用一句话表达新节点在集合之前/之后make_edges用参数位置表达批量连边方向多端口节点则借助按索引匹配的规则实现multifunction_node、split_node、join_node、indexer_node的简洁构图。理解order标记的相对语义following集合是前驱、preceding集合是后继是正确使用它的关键而它的同一图假设集合大小须等于端口数暂不支持composite_node等约束与开放问题也决定了它目前仍是面向反馈收集的预览特性尚未进入 oneTBB 正式规范。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考