7 函数
7.1 模式匹配
函数头以及 case 和 receive 子句中的模式匹配都由编译器进行优化。除了少数例外,重新排列子句不会带来任何好处。
一个例外是二进制的模式匹配。编译器不会重新排列匹配二进制的子句。将匹配空二进制的子句放在最后通常比放在最前面稍微快一些。
以下是一个相当不自然的示例,用于展示另一个例外
不要
atom_map1(one) -> 1; atom_map1(two) -> 2; atom_map1(three) -> 3; atom_map1(Int) when is_integer(Int) -> Int; atom_map1(four) -> 4; atom_map1(five) -> 5; atom_map1(six) -> 6.
问题在于包含变量 Int 的子句。由于变量可以匹配任何内容,包括原子 four、five 和 six(后面的子句也匹配这些原子),因此编译器必须生成次优代码,执行过程如下
- 首先,输入值将与 one、two 和 three 进行比较(使用一个执行二进制搜索的单个指令;因此,即使有许多值,效率也很高)以选择要执行的前三个子句中的哪一个(如果有)。
- 如果前三个子句都没有匹配,则第四个子句匹配,因为变量总是匹配。
- 如果保护测试 is_integer(Int) 成功,则执行第四个子句。
- 如果保护测试失败,则输入值将与 four、five 和 six 进行比较,并选择相应的子句。(如果没有任何值匹配,则会发生 function_clause 异常。)
重写为以下任一
做
atom_map2(one) -> 1; atom_map2(two) -> 2; atom_map2(three) -> 3; atom_map2(four) -> 4; atom_map2(five) -> 5; atom_map2(six) -> 6; atom_map2(Int) when is_integer(Int) -> Int.
或
做
atom_map3(Int) when is_integer(Int) -> Int; atom_map3(one) -> 1; atom_map3(two) -> 2; atom_map3(three) -> 3; atom_map3(four) -> 4; atom_map3(five) -> 5; atom_map3(six) -> 6.
可以生成稍微更高效的匹配代码。
另一个例子
不要
map_pairs1(_Map, [], Ys) -> Ys; map_pairs1(_Map, Xs, [] ) -> Xs; map_pairs1(Map, [X|Xs], [Y|Ys]) -> [Map(X, Y)|map_pairs1(Map, Xs, Ys)].
第一个参数不是问题。它是一个变量,但在所有子句中都是一个变量。问题是第二个参数中的变量 Xs,位于中间子句中。因为变量可以匹配任何东西,所以编译器不允许重新排列子句,而必须生成按书写顺序匹配它们的代码。
如果将函数重写如下,编译器可以自由地重新排列子句
做
map_pairs2(_Map, [], Ys) -> Ys; map_pairs2(_Map, [_|_]=Xs, [] ) -> Xs; map_pairs2(Map, [X|Xs], [Y|Ys]) -> [Map(X, Y)|map_pairs2(Map, Xs, Ys)].
编译器将生成类似于以下代码的代码
不要(编译器已经完成)
explicit_map_pairs(Map, Xs0, Ys0) -> case Xs0 of [X|Xs] -> case Ys0 of [Y|Ys] -> [Map(X, Y)|explicit_map_pairs(Map, Xs, Ys)]; [] -> Xs0 end; [] -> Ys0 end.
对于输入列表不是空列表或很短的列表的最常见情况,这会稍微快一些。(另一个好处是 Dialyzer 可以为 Xs 变量推断出更好的类型。)
7.2 函数调用
这是不同类型的函数调用性能的粗略层次结构
- 对本地或外部函数的调用(foo()、m:foo())是最快的调用。
- 调用或应用一个 fun(Fun()、apply(Fun, []))只比外部调用慢一点。
- 应用一个导出函数(Mod:Name()、apply(Mod, Name, [])),其中参数数量在编译时已知,排名第二。
- 应用一个导出函数(apply(Mod, Name, Args)),其中参数数量在编译时未知,效率最低。
注释和实现细节
调用和应用 fun 不涉及任何哈希表查找。fun 包含一个指向实现 fun 的函数的(间接)指针。
apply/3 必须在哈希表中查找要执行的函数的代码。因此,它总是比直接调用或 fun 调用慢。
将回调函数缓存到 fun 中,从长远来看,可能比对常用回调进行 apply 调用更高效。
7.3 递归中的内存使用
在编写递归函数时,最好使它们成为尾递归,以便它们可以在恒定内存空间中执行
做
list_length(List) -> list_length(List, 0). list_length([], AccLen) -> AccLen; % Base case list_length([_|Tail], AccLen) -> list_length(Tail, AccLen + 1). % Tail-recursive
不要
list_length([]) -> 0. % Base case list_length([_ | Tail]) -> list_length(Tail) + 1. % Not tail-recursive