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

    1. // Bad Code!
  • Good Example

    1. // Good Code!

1.使用规定的遍历算法

  • 约定

    flume/planner目录下提供了若干种格式化的遍历方法, 他们包括基于从属关系的先序/中序/后序遍历, 基于数据流的正/反拓扑序遍历, 基于Task的遍历等. 我们编写的所有算法, 都必须基于这些机制进行遍历.

    ‘自由’的遍历方式往往造成难以阅读的代码. 另外, 通过一定程度的训练和适应, 一旦接受现有的设定, 会发现还是可以比较自如的实现各种图算法的.

  • Bad Example

    1. class DoSomethingRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return unit == plan->Root();
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. FindUnitsAndDoSomething(unit);
    8. return true;
    9. }
    10. void FindUnitsAndDoSomething(Unit* unit) { // BAD CODE!
    11. DoSomething(unit);
    12. BOOST_FOREACH(unit* child, unit->children()) {
    13. FindUnitsAndDoSomething(child);
    14. }
    15. }
    16. void DoSomething(Unit* unit) {}
    17. };
    18. RuleDispatcher dispatcher;
    19. dispatcher.AddRule(new DoSomethingRule);
    20. dispatcher.Run();
  • Good Example

    1. class DoSomethingRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return true;
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. return DoSomething(unit);
    8. }
    9. bool DoSomething(Unit* unit) { return true; }
    10. };
    11. DepthFirstDispatcher dispatcher(DepthFirstDispatcher::PRE_ORDER);
    12. dispatcher.AddRule(new DoSomethingRule);
    13. dispatcher.Run();

2.把完整判断放入Accept函数中

  • 约定

    所有的遍历算法都需要实现Accept和Run方法. 尽量把所有的判断逻辑放在Accept中, 而不是分布在两个函数中.

  • Bad Example

    1. class DoSomethingRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return unit->type() == Unit::LOCAL_SHUFFLE;
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. if (unit->father() != unit->task()) { // BAD CODE!
    8. return false;
    9. }
    10. return DoSomething(unit);
    11. }
    12. bool DoSomething(Unit* unit) { return true; }
    13. };
  • Good Example

    1. class DoSomethingRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return unit->type() == Unit::LOCAL_SHUFFLE && unit->father() == unit->task();
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. return DoSomething(unit);
    8. }
    9. bool DoSomething(Unit* unit) { return true; }
    10. };

3.避免在Rule::Run函数中做二次遍历

  • 约定

    尽量利用Accept方法将某个Rule的作用锚定在直接操作的节点上, 而不是锚定在上游/父节点上.

  • Bad Example

    1. class DoSomethingRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return unit->type() == Unit::LOCAL_SHUFFLE;
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. bool is_changed = false;
    8. BOOST_FOREACH(unit* child, unit->children()) { // BAD CODE!
    9. is_changed ||= DoSomething(child);
    10. }
    11. return is_changed;
    12. }
    13. bool DoSomething(Unit* unit) { return true; }
    14. };
  • Good Example

    1. class DoSomethingRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return unit->father()->type() == Unit::LOCAL_SHUFFLE;
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. return DoSomething(unit);
    8. }
    9. bool DoSomething(Unit* unit) { return true; }
    10. };

4.在Rule中只访问周围节点

  • 约定

    Unit中提供了方法, 能够得到某个节点的父/子/直接上下游节点. 同时, 如DataFlowAnalysis这样的公共Pass提供了访问所有前继/后继/子孙的方法.

    我们约定, 在相似的实现代价下, 优先采用只访问周围节点的算法. 这样的实现往往更容易理解, 同时能够减少不必要的外部依赖.

  • Bad Example

    1. struct DesendantCount {
    2. int value;
    3. DesendantCount() : value(0) {}
    4. };
    5. class CountDesendantRule : public RuleDispatcher::Rule {
    6. public:
    7. virtual bool Accept(Plan* plan, Unit* unit) {
    8. return true;
    9. }
    10. virtual bool Run(Plan* plan, Unit* unit) {
    11. DataFlow& dataflow = unit->get<DataFlow>(); // BAD CODE!
    12. unit->get<DesendantCount>().value = dataflow.nodes.size();
    13. return true;
    14. }
    15. };
  • Good Example

    1. class CountDesendantRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return true;
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. int& sum = unit->get<DesendantCount>().value;
    8. BOOST_FOREACH(unit* child, unit->children()) {
    9. sum += child->get<DesendantCount>().value;
    10. }
    11. return true;
    12. }
    13. };
    14. DepthFirstDispatcher dispatcher(DepthFirstDispatcher::POST_ORDER);
    15. dispatcher.AddRule(new DoSomethingRule);
    16. dispatcher.Run();

5.保持一个Rule内的逻辑尽量简单

  • 约定

    Rule为Planner代码编写过程中的最小单位. 一个Rule内包含Accept和Run两个方法, 这两个方法需被视为整体看待.

    类似于一般程序编写中不要定义过长的语句, 我们在Planner中也不要编写职责过于复杂的Rule. 如果一个Rule中的逻辑过于复杂, 我们要尽量分拆这个Rule.

  • Bad Example

    1. class DoManyThingRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return true;
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. // BAD CODE!
    8. DoFirstThing();
    9. DoSecondThing();
    10. DoLastThing();
    11. return true;
    12. }
    13. bool DoFirstThing();
    14. bool DoSecondThing();
    15. bool DoLastThing();
    16. };
  • Good Example

    1. class DoFirstThingRule : public RuleDispatcher::Rule {
    2. public:
    3. virtual bool Accept(Plan* plan, Unit* unit) {
    4. return true;
    5. }
    6. virtual bool Run(Plan* plan, Unit* unit) {
    7. return DoFirstThing();
    8. }
    9. bool DoFirstThing();
    10. };
    11. class DoSecondThingRule : public RuleDispatcher::Rule {
    12. public:
    13. virtual bool Accept(Plan* plan, Unit* unit) {
    14. return true;
    15. }
    16. virtual bool Run(Plan* plan, Unit* unit) {
    17. return DoSecondThing();
    18. }
    19. bool DoSecondThing();
    20. };
    21. class DoLastThingRule : public RuleDispatcher::Rule {
    22. public:
    23. virtual bool Accept(Plan* plan, Unit* unit) {
    24. return true;
    25. }
    26. virtual bool Run(Plan* plan, Unit* unit) {
    27. return DoLastThing();
    28. }
    29. bool DoLastThing();
    30. };
    31. RuleDispatcher dispatcher;
    32. dispatcher.AddRule(new DoFirstThingRule);
    33. dispatcher.AddRule(new DoSecondThingRule);
    34. dispatcher.AddRule(new DoLastThingRule);
    35. dispatcher.Run();

6.只使用Tag保存状态和中间结果

  • 约定

    有三个地方可以保存计算中间结果: 全局变量, Rule中定义的成员变量, Unit节点上记录的Tag标记. 我们约定, 只允许 在Unit上通过Tag保存中间计算结果.

    在全局变量上保存中间结果容易引起并发和扩展性问题. 成员变量上保存的结果不方便在Rule之间分享, 并且往往会使Rule的设计变得更加复杂. 故作此约定.

  • Bad Example

    1. struct AllNodesCount {
    2. int value;
    3. AllNodesCount() : value(0) {}
    4. };
    5. class CountAllNodesRule : public RuleDispatcher::Rule {
    6. public:
    7. CountAllNodesRule() : m_count(0) {}
    8. virtual bool Accept(Plan* plan, Unit* unit) {
    9. return true;
    10. }
    11. virtual bool Run(Plan* plan, Unit* unit) {
    12. if (unit == plan->Root()) {
    13. unit->get<AllNodesCount>().value = m_count + 1;
    14. } else {
    15. ++m_count;
    16. }
    17. return true;
    18. }
    19. private:
    20. int m_count; // BAD CODE!
    21. };
  • Good Example

    1. // By POST_ORDER
    2. class CountAllNodesRule : public RuleDispatcher::Rule {
    3. public:
    4. virtual bool Accept(Plan* plan, Unit* unit) {
    5. return true;
    6. }
    7. virtual bool Run(Plan* plan, Unit* unit) {
    8. int count = 1; // for myself
    9. BOOST_FOREACH(unit* child, unit->children()) {
    10. count += child->get<AllNodesCount>();
    11. }
    12. unit->get<AllNodesCount>().value = count;
    13. return true;
    14. }
    15. };

7.不要使用公共类型作为Tag

  • 约定

    Tag机制的实现依赖于C++的类型系统, 在Unit上可以为每一种C++类型保存唯一实例. 如果用公共类型, 如std::map/std::string等作为Tag类型, 则很容易和其它Pass产生冲突/混淆. 因此不要使用这些类型作为Tag.

    另外, 我们约定各种Proto类型只能由相应的BuildXxxMessagePass使用.

  • Bad Example

    1. typedef std::set<Unit*> NodeSetTag;
  • Good Example

    1. 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

    1. class DoSomethingPass : public Pass {
    2. public:
    3. virtual bool Run(Plan* plan) {
    4. PerformFirstStep(plan);
    5. DataFlowAnalysis().Run(plan); // BAD CODE!
    6. PerformSecondStep(plan);
    7. }
    8. void PerformFirstStep(Plan* plan);
    9. void PerformSecondStep(Plan* plan);
    10. };
  • Good Example

    1. namespace internal {
    2. class PerformFirstStepPass : public Pass {
    3. RELY_PASS(DataFlowAnalysis);
    4. };
    5. class PerformSecondStepPass : public Pass {
    6. RELY_PASS(DataFlowAnalysis);
    7. RELY_PASS(PerformFirstStepPass);
    8. };
    9. } // namespace internal
    10. class DoSomethingPass : public Pass {
    11. RELY_PASS(PerformFirstStepPass);
    12. RELY_PASS(PerformSecondStepPass);
    13. };

9.保证Run方法的返回值正确

  • 约定

    Rule::Run和Pass::Run的返回值代表Plan的拓扑或者关键信息是否发生改变. 有时程序员为了简单实现, 会让这两个方法总是返回true. 这种做法容易导致死循环, 并且使得优化中间步骤变多, 影响调试. 因此我们约定, 实现Pass时一定要保证返回值正确.

  • Bad Example

    1. struct Tag {};
    2. class SetTagRule : public RuleDispatcher::Rule {
    3. public:
    4. virtual bool Accept(Plan* plan, Unit* unit) {
    5. return unit == plan->Root();
    6. }
    7. virtual bool Run(Plan* plan, Unit* unit) {
    8. unit->set<Tag>();
    9. return true; // BAD CODE!
    10. }
    11. };
  • Good Example

    1. struct Tag {};
    2. class SetTagRule : public RuleDispatcher::Rule {
    3. public:
    4. virtual bool Accept(Plan* plan, Unit* unit) {
    5. return unit == plan->Root();
    6. }
    7. virtual bool Run(Plan* plan, Unit* unit) {
    8. bool is_modified = !unit->has<Tag>();
    9. unit->set<Tag>();
    10. return is_modified;
    11. }
    12. };