关于两条相关经验法则的简短说明。
原文链接:Push Ifs Up And Fors Down(2023 年 11 月 15 日)
把 If 上推
如果函数内部有一个 if 条件,考虑是否应该把它移到调用方:
// ✅ GOOD
fn frobnicate(walrus: Walrus) {
...
}
// ❌ BAD
fn frobnicate(walrus: Option<Walrus>) {
let walrus = match walrus {
Some(it) => it,
None => return,
};
...
}
如上面的例子所示,这种情况常常出现在前置条件上:函数可以在内部检查前置条件,不成立就“什么都不做”;也可以把检查的任务推给调用方,并通过类型(或断言)来保证前置条件成立。尤其对于前置条件来说,“上推”可能像病毒一样传播,并最终减少总体检查次数——这是这条经验法则的动机之一。
另一个动机是:控制流和 if 很复杂,也是 bug 的来源。把 if 上推之后,你常常会把控制流集中到一个函数里——这个函数有复杂的分支逻辑,而所有实际工作都委托给直线型的子例程。
如果你确实有复杂的控制流,最好把它放在一个函数里、一屏就能看完,而不是散布在整个文件中。更重要的是,当所有流程都在一个地方时,常常能发现冗余和死条件。比较:
fn f() {
if foo && bar {
if foo {
} else {
}
}
}
fn g() {
if foo && bar {
h()
}
}
fn h() {
if foo {
} else {
}
}
对于 f,注意到死分支要比对 g 和 h 的组合容易得多!
一个相关的模式:溶解枚举
这里一个相关的模式是我所称的「溶解枚举」(dissolving enum)重构。有时代码会变成这样:
enum E {
Foo(i32),
Bar(String),
}
fn main() {
let e = f();
g(e)
}
fn f() -> E {
if condition {
E::Foo(x)
} else {
E::Bar(y)
}
}
fn g(e: E) {
match e {
E::Foo(x) => foo(x),
E::Bar(y) => bar(y)
}
}
这里有两个分支指令;把它们上提之后,就会明显看出:这是完全相同的条件,被重复了三遍(第三次具体化为一个数据结构)。
fn main() {
if condition {
foo(x)
} else {
bar(y)
}
}
把 For 下推
这条来自面向数据的思路流派。少的东西就是少,多的东西就是多。程序通常处理成批的对象;或者至少,热路径通常涉及处理许多实体——正是实体的数量让这条路径变热。
所以,引入对象「批次」(batch)的概念,并让对批量的操作成为基本情形,而标量版本成为批量版本的特例,往往是明智的:
// ✅ GOOD
frobnicate_batch(walruses)
// ❌ BAD
for walrus in walruses {
frobnicate(walrus)
}
这里的主要好处是性能——大量的性能,在极端情况下尤其如此。
如果你有一整批东西要处理,就可以摊薄启动成本,并灵活安排处理顺序。事实上,你甚至不必按任何特定顺序处理实体:你可以使用向量化 / 结构体数组(struct-of-array)技巧,先处理所有实体的某一个字段,然后再继续处理其他字段。
也许这里最有趣的例子是基于 FFT 的多项式乘法:事实证明,同时在许多点上求值一个多项式,可能比逐点分别求值更快!
两条建议可以组合
关于 for 和 if 的两条建议甚至可以组合起来:
// ✅ GOOD
if condition {
for walrus in walruses {
walrus.frobnicate()
}
} else {
for walrus in walruses {
walrus.transmogrify()
}
}
// ❌ BAD
for walrus in walruses {
if condition {
walrus.frobnicate()
} else {
walrus.transmogrify()
}
}
GOOD 版本很好,因为:
- 它避免反复重新求值
condition; - 它从热循环中移除了一个分支;
- 它可能解锁向量化。
这个模式在微观和宏观层面都适用——GOOD 版本就是 TigerBeetle 的架构:在数据平面中,我们同时处理成批对象,以摊薄控制平面中做决策的成本。
虽然性能也许是 for 这条建议的主要动机,但有时它也有助于表达力。jQuery 当年相当成功,它就是作用于元素集合的。抽象向量空间的语言,往往比一堆逐坐标方程更适合作为思考工具。
总之,把 if 上推,把 for 下推!