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;
}
};