Planner编码规范¶
为了统一Flume项目中Planner部分的编码风格, 增强代码的可读性和可维护性, 这里对Pass/Rule的编写/组织作出约定, 开发者在为Planner编写代码之前, 请务必阅读此规范. 关于Planner的基础知识, 请参见 优化器框架
前言¶
Planner的编程需遵守以下原则.
1.可读性优先¶
Planner部分的代码基本都是基于图的计算和变换, 代码很容易变得晦涩难懂. 在性能/可读性/可扩展性这三个代码基本评判标准之间, 我们认为可读性>可扩展性>性能.
Planner中的代码往往只在任务提交时运行一遍, 且在一般计算中的算子数量往往有限, 故执行计划的分析和优化绝不会成为整个任务的瓶颈, 因此性能因素在Planner的编码过程中通常可以不予考虑.
原则上, 我们希望一个Pass可以被复用在多个Backend上, 出于这种考虑, 我们可以将某个Pass设计的较为复杂, 适应更广泛的边界条件, 但是这种复杂不应该影响到代码的可读性. 可读性是一切复用的基础.
2.遵循范式¶
基于图的算法在实现上往往具有较大的自由度, 尤其在C++这样的混合型语言上, 同一种遍历过程都可以有多种写法. 不幸的是, 这种自由度往往会对代码的阅读和理解造成严重干扰.
Flume在Planner中提供了一些机制, 如利用PassManager来管理Pass之间的依赖, 利用Dispatcher/Rule的来规范化遍历过程, 提供机制可以在每个图节点上加标记(Tag), 这些机制存在的主要目的是固化相应的 编程范式, 而不在于提供编程方便性.
我们在为Planner编写代码时, 不应该 试图绕过这些机制, 或是自行实现类似机制. 严格遵循范式, 在牺牲编程自由度的同时, 可以方便代码阅读者把更多的精力关注到图算法上, 这一点尤其重要.
3.代码组织原则¶
所有的Planner代码, 要按照入口Pass/内部Pass/Rule的原则来组织(入口/内部Pass参见 8.禁止显式调用Pass的Run方法). Pass/Rule本身应该是无状态的(常量除外), 所有的计算结果(包括中间结果)都应该体现在Plan/Unit上, 这一点和过程式编程语言十分相似. 对应的, 我们可以把入口Pass看作是模块, 把内部Pass看成是方法, Rule看成是语句.
每个编译单元中有且只有一个入口Pass, 其中应该包含尽量完整的逻辑. 功能相同, 或者逻辑相似但使用范围不同的Pass, 应该尽量被整合到一个Pass文件中. 分散定义且数量很多的Pass文件往往不利于代码阅读和维护, 同时也不利用公共逻辑的抽取和复用. 如果某一Analysis只对某一个Pass产生作用, 建议将它和该Pass合并到同一文件中去.
和Pass相反, 作为'语句'级的编程结构, 每个Rule中包含的逻辑要尽量简单易懂. 对于复杂的Rule, 要利用 3.避免在Rule::Run函数中做二次遍历, 4.在Rule中只访问周围节点, 5.保持一个Rule内的逻辑尽量简单 中描述的技巧进行拆解.
另外, 我们往往利用在节点打Tag的方式, 在Pass&Rule之间传递信息. 这里要注意的是, 如果某个头文件暴露了过多的公共Tag, 往往说明这个模块和其它模块之间的职责分配不够清晰, 纠缠在一起的多个Pass会增加阅读的难度. 编程的时候, 要尽量设法避免暴露不必要的Tag.
规范¶
0.文档范式¶
约定
描述信息
- Bad Example
// Bad Code!
- Good Example
// Good Code!
1.使用规定的遍历算法¶
约定
flume/planner目录下提供了若干种格式化的遍历方法, 他们包括基于从属关系的先序/中序/后序遍历, 基于数据流的正/反拓扑序遍历, 基于Task的遍历等. 我们编写的所有算法, 都必须基于这些机制进行遍历.
'自由'的遍历方式往往造成难以阅读的代码. 另外, 通过一定程度的训练和适应, 一旦接受现有的设定, 会发现还是可以比较自如的实现各种图算法的.
- Bad Example
class DoSomethingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return unit == plan->Root(); } virtual bool Run(Plan* plan, Unit* unit) { FindUnitsAndDoSomething(unit); return true; } void FindUnitsAndDoSomething(Unit* unit) { // BAD CODE! DoSomething(unit); BOOST_FOREACH(unit* child, unit->children()) { FindUnitsAndDoSomething(child); } } void DoSomething(Unit* unit) {} }; RuleDispatcher dispatcher; dispatcher.AddRule(new DoSomethingRule); dispatcher.Run();
- Good Example
class DoSomethingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { return DoSomething(unit); } bool DoSomething(Unit* unit) { return true; } }; DepthFirstDispatcher dispatcher(DepthFirstDispatcher::PRE_ORDER); dispatcher.AddRule(new DoSomethingRule); dispatcher.Run();
2.把完整判断放入Accept函数中¶
约定
所有的遍历算法都需要实现Accept和Run方法. 尽量把所有的判断逻辑放在Accept中, 而不是分布在两个函数中.
- Bad Example
class DoSomethingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return unit->type() == Unit::LOCAL_SHUFFLE; } virtual bool Run(Plan* plan, Unit* unit) { if (unit->father() != unit->task()) { // BAD CODE! return false; } return DoSomething(unit); } bool DoSomething(Unit* unit) { return true; } };
- Good Example
class DoSomethingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return unit->type() == Unit::LOCAL_SHUFFLE && unit->father() == unit->task(); } virtual bool Run(Plan* plan, Unit* unit) { return DoSomething(unit); } bool DoSomething(Unit* unit) { return true; } };
3.避免在Rule::Run函数中做二次遍历¶
约定
尽量利用Accept方法将某个Rule的作用锚定在直接操作的节点上, 而不是锚定在上游/父节点上.
- Bad Example
class DoSomethingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return unit->type() == Unit::LOCAL_SHUFFLE; } virtual bool Run(Plan* plan, Unit* unit) { bool is_changed = false; BOOST_FOREACH(unit* child, unit->children()) { // BAD CODE! is_changed ||= DoSomething(child); } return is_changed; } bool DoSomething(Unit* unit) { return true; } };
- Good Example
class DoSomethingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return unit->father()->type() == Unit::LOCAL_SHUFFLE; } virtual bool Run(Plan* plan, Unit* unit) { return DoSomething(unit); } bool DoSomething(Unit* unit) { return true; } };
4.在Rule中只访问周围节点¶
约定
Unit中提供了方法, 能够得到某个节点的父/子/直接上下游节点. 同时, 如DataFlowAnalysis这样的公共Pass提供了访问所有前继/后继/子孙的方法.
我们约定, 在相似的实现代价下, 优先采用只访问周围节点的算法. 这样的实现往往更容易理解, 同时能够减少不必要的外部依赖.
- Bad Example
struct DesendantCount { int value; DesendantCount() : value(0) {} }; class CountDesendantRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { DataFlow& dataflow = unit->get<DataFlow>(); // BAD CODE! unit->get<DesendantCount>().value = dataflow.nodes.size(); return true; } };
- Good Example
class CountDesendantRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { int& sum = unit->get<DesendantCount>().value; BOOST_FOREACH(unit* child, unit->children()) { sum += child->get<DesendantCount>().value; } return true; } }; DepthFirstDispatcher dispatcher(DepthFirstDispatcher::POST_ORDER); dispatcher.AddRule(new DoSomethingRule); dispatcher.Run();
5.保持一个Rule内的逻辑尽量简单¶
约定
Rule为Planner代码编写过程中的最小单位. 一个Rule内包含Accept和Run两个方法, 这两个方法需被视为整体看待.
类似于一般程序编写中不要定义过长的语句, 我们在Planner中也不要编写职责过于复杂的Rule. 如果一个Rule中的逻辑过于复杂, 我们要尽量分拆这个Rule.
- Bad Example
class DoManyThingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { // BAD CODE! DoFirstThing(); DoSecondThing(); DoLastThing(); return true; } bool DoFirstThing(); bool DoSecondThing(); bool DoLastThing(); };
- Good Example
class DoFirstThingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { return DoFirstThing(); } bool DoFirstThing(); }; class DoSecondThingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { return DoSecondThing(); } bool DoSecondThing(); }; class DoLastThingRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { return DoLastThing(); } bool DoLastThing(); }; RuleDispatcher dispatcher; dispatcher.AddRule(new DoFirstThingRule); dispatcher.AddRule(new DoSecondThingRule); dispatcher.AddRule(new DoLastThingRule); dispatcher.Run();
6.只使用Tag保存状态和中间结果¶
约定
有三个地方可以保存计算中间结果: 全局变量, Rule中定义的成员变量, Unit节点上记录的Tag标记. 我们约定, 只允许 在Unit上通过Tag保存中间计算结果.
在全局变量上保存中间结果容易引起并发和扩展性问题. 成员变量上保存的结果不方便在Rule之间分享, 并且往往会使Rule的设计变得更加复杂. 故作此约定.
- Bad Example
struct AllNodesCount { int value; AllNodesCount() : value(0) {} }; class CountAllNodesRule : public RuleDispatcher::Rule { public: CountAllNodesRule() : m_count(0) {} virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { if (unit == plan->Root()) { unit->get<AllNodesCount>().value = m_count + 1; } else { ++m_count; } return true; } private: int m_count; // BAD CODE! };
- Good Example
// By POST_ORDER class CountAllNodesRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return true; } virtual bool Run(Plan* plan, Unit* unit) { int count = 1; // for myself BOOST_FOREACH(unit* child, unit->children()) { count += child->get<AllNodesCount>(); } unit->get<AllNodesCount>().value = count; return true; } };
7.不要使用公共类型作为Tag¶
约定
Tag机制的实现依赖于C++的类型系统, 在Unit上可以为每一种C++类型保存唯一实例. 如果用公共类型, 如std::map/std::string等作为Tag类型, 则很容易和其它Pass产生冲突/混淆. 因此不要使用这些类型作为Tag.
另外, 我们约定各种Proto类型只能由相应的BuildXxxMessagePass使用.
- Bad Example
typedef std::set<Unit*> NodeSetTag;
- Good Example
class NodeSetTag : public std::set<Unit*> {};
8.禁止显式调用Pass的Run方法¶
约定
PassManager中定义了Pass之间的依赖关系, 每个Pass可以在定义的时候声明自己依赖哪些Pass, 然后通过PassManager中的Apply方法来调用Pass.
统一的依赖入口, 可以方便我们对代码做调整和拆分, 同时PassManager在调用Pass会记录相应的调试信息, 方便我们跟踪线上问题. 为了维护代码的统一性, 我们禁止在单测之外直接调用Pass::Run方法.
一些Pass在遍历过程中会改变拓扑, 从而破坏所依赖的Analysis结果. 遇到这种情形, 建议实现者多思考一下, 大部分情况下可以通过调整算法规避这些问题. 如果确实无法避免, 可以将一个Pass拆分成入口Pass和内部Pass.
- Bad Example
class DoSomethingPass : public Pass { public: virtual bool Run(Plan* plan) { PerformFirstStep(plan); DataFlowAnalysis().Run(plan); // BAD CODE! PerformSecondStep(plan); } void PerformFirstStep(Plan* plan); void PerformSecondStep(Plan* plan); };
- Good Example
namespace internal { class PerformFirstStepPass : public Pass { RELY_PASS(DataFlowAnalysis); }; class PerformSecondStepPass : public Pass { RELY_PASS(DataFlowAnalysis); RELY_PASS(PerformFirstStepPass); }; } // namespace internal class DoSomethingPass : public Pass { RELY_PASS(PerformFirstStepPass); RELY_PASS(PerformSecondStepPass); };
9.保证Run方法的返回值正确¶
约定
Rule::Run和Pass::Run的返回值代表Plan的拓扑或者关键信息是否发生改变. 有时程序员为了简单实现, 会让这两个方法总是返回true. 这种做法容易导致死循环, 并且使得优化中间步骤变多, 影响调试. 因此我们约定, 实现Pass时一定要保证返回值正确.
- Bad Example
struct Tag {}; class SetTagRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return unit == plan->Root(); } virtual bool Run(Plan* plan, Unit* unit) { unit->set<Tag>(); return true; // BAD CODE! } };
- Good Example
struct Tag {}; class SetTagRule : public RuleDispatcher::Rule { public: virtual bool Accept(Plan* plan, Unit* unit) { return unit == plan->Root(); } virtual bool Run(Plan* plan, Unit* unit) { bool is_modified = !unit->has<Tag>(); unit->set<Tag>(); return is_modified; } };