[Erlang-0007][OTP] 高效指南 -- 函数
6 函数
6.1 模式匹配
函数头以及case和receive子句中的模式匹配都被编译器优化过。除了个别例外,大部分情况下调整顺序不会带来任何好处。
二进制就是一个例外。匹配二进制时,编译器不会重新排列分支。把一个空的二进制放在后边通常会比放在前面稍快。
下面的例子展示了另一种例外情况:
DO NOT 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的异常。)
可以重写为下面两种:
DO 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.
or
DO 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.
这样会使匹配代码更高效些。
这里有一个不太恰当的例子:
DO NOT 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,在中间一个分支。以为变量能匹配一切,编译器不会自动重排分支,而必须按照顺序生成代码。
如果这个函数像这样重写:
DO 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)].
编译器就可以随意的重排分支,会生成像这样的代码:
DO NOT (already done by the compiler) 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的类型。)
6.2 函数调用
这里有一个不同类型的函数调用的相对代价的粗略指引。基准数字是在Solaris/Sparc上得出的:
- 本地调用和外部调用(foo(),m:foo())是最快的。
- 调用fun函数(Fun(),apply(Fun, []))代价大约会比本地调用大三倍。
- 调用一个导出函数(Mod:Name(),Apply(Mod,Name,[]))代价两倍于调用fun函数,六倍于调用本地函数。
注意事项和实现细节
调用fun函数不需要查询哈希表。一个fun函数包含只想函数具体实现的指针。
注意: 元组不是fun函数,一个“tuple fun”,{Module,Function},不是fun函数。调用“tuple fun”的代价和apply/3差不多,甚至跟糟糕。极其不推荐使用“tuple funs”,而且有可能在今后的发布版本中这种方式将不会被支持,因为自从R10B开始就有一个超级替代品,叫做fun Module:Function/Arity语法。
apply/3必须通过一个哈希表查询代码找到函数再执行。因此,比直接调用或fun要慢。
你不需要再关心如何这样写(从性能的角度出发)
Module:Function(Arg1, Arg2)
还是
apply(Module, Function, [Arg1,Arg2])
(编译器会把下面一种写法优化成上面那种)
下面的代码
apply(Module, Function, Arguments)
会稍微慢一点点,因为编译器不知道参数的个数。
6.3 递归的内存使用
当写递归函数的时候最好写成尾递归,以便执行的时候只使用固定大小的内存空间。
DO list_length(List) -> list_length(List, 0). list_length([], AccLen) -> AccLen; % Base case list_length([_|Tail], AccLen) -> list_length(Tail, AccLen + 1). % Tail-recursive
DO NOT list_length([]) -> 0. % Base case list_length([_ | Tail]) -> list_length(Tail) + 1. % Not tail-recursive