7  函数

7  函数

函数头以及 casereceive 子句中的模式匹配都由编译器进行优化。除了少数例外,重新排列子句不会带来任何好处。

一个例外是二进制的模式匹配。编译器不会重新排列匹配二进制的子句。将匹配空二进制的子句放在最后通常比放在最前面稍微快一些。

以下是一个相当不自然的示例,用于展示另一个例外

不要

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 的子句。由于变量可以匹配任何内容,包括原子 fourfivesix(后面的子句也匹配这些原子),因此编译器必须生成次优代码,执行过程如下

  • 首先,输入值将与 onetwothree 进行比较(使用一个执行二进制搜索的单个指令;因此,即使有许多值,效率也很高)以选择要执行的前三个子句中的哪一个(如果有)。
  • 如果前三个子句都没有匹配,则第四个子句匹配,因为变量总是匹配。
  • 如果保护测试 is_integer(Int) 成功,则执行第四个子句。
  • 如果保护测试失败,则输入值将与 fourfivesix 进行比较,并选择相应的子句。(如果没有任何值匹配,则会发生 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 变量推断出更好的类型。)

这是不同类型的函数调用性能的粗略层次结构

  • 对本地或外部函数的调用(foo()m:foo())是最快的调用。
  • 调用或应用一个 fun(Fun()apply(Fun, []))只比外部调用慢一点。
  • 应用一个导出函数(Mod:Name()apply(Mod, Name, [])),其中参数数量在编译时已知,排名第二。
  • 应用一个导出函数(apply(Mod, Name, Args)),其中参数数量在编译时未知,效率最低。

调用和应用 fun 不涉及任何哈希表查找。fun 包含一个指向实现 fun 的函数的(间接)指针。

apply/3 必须在哈希表中查找要执行的函数的代码。因此,它总是比直接调用或 fun 调用慢。

将回调函数缓存到 fun 中,从长远来看,可能比对常用回调进行 apply 调用更高效。

在编写递归函数时,最好使它们成为尾递归,以便它们可以在恒定内存空间中执行

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