2  Erlang 性能的七个神话

2 Erlang 性能的七个神话

一些真理似乎比其最佳使用期限存活的时间更长,也许是因为 "信息" 人与人之间传播的速度比一篇单一的发布说明快,例如,说主体递归调用已经变得更快了。

本节试图消除已经成为神话的旧真理(或半真理)。

根据神话,使用一个尾递归函数,它反向构建一个列表,然后调用 lists:reverse/1,比一个主体递归函数更快,该函数按正确顺序构建列表;原因是主体递归函数比尾递归函数使用更多内存。

在 R12B 之前,这在一定程度上是正确的。在 R7B 之前,情况更加如此。今天,情况并非如此。一个主体递归函数通常使用与尾递归函数相同的内存量。通常无法预测尾递归版本还是主体递归版本更快。因此,使用使你的代码更简洁的版本(提示:通常是主体递归版本)。

有关尾递归和主体递归的更深入讨论,请参见 Erlang 的尾递归不是灵丹妙药

注意

一个不需要在最后反转列表的尾递归函数比一个主体递归函数更快,尾递归函数也不构造任何项(例如,一个函数,它对列表中的所有整数求和)。

++ 操作符多少有些名不副实地获得了不好的名声。这可能与以下代码有关,这是反转列表最没有效率的方式

不要

naive_reverse([H|T]) ->
    naive_reverse(T)++[H];
naive_reverse([]) ->
    [].

因为 ++ 操作符会复制其左操作数,因此结果会重复复制,导致二次复杂度。

但像这样使用 ++ 并不糟糕

可以

naive_but_ok_reverse([H|T], Acc) ->
    naive_but_ok_reverse(T, [H]++Acc);
naive_but_ok_reverse([], Acc) ->
    Acc.

每个列表元素只复制一次。正在增长的结果 Acc++ 操作符的右操作数,它不会被复制。

经验丰富的 Erlang 程序员会这样写

vanilla_reverse([H|T], Acc) ->
    vanilla_reverse(T, [H|Acc]);
vanilla_reverse([], Acc) ->
    Acc.

这稍稍更有效率,因为这里你不会只构建一个列表元素,然后直接复制它。(或者,如果编译器没有自动将 [H]++Acc 重写为 [H|Acc],它会更有效。)

如果处理不当,字符串处理可能会很慢。在 Erlang 中,你需要更多地考虑字符串的使用方式,并选择合适的表示形式。如果你使用正则表达式,请使用 STDLIB 中的 re 模块,而不是过时的 regexp 模块。

修复时间仍然与文件中的记录数量成正比,但 Dets 的修复在过去曾经慢得多。Dets 已经进行了大规模的重写和改进。

BEAM 是一个基于寄存器的虚拟机。它有 1024 个虚拟寄存器,用于保存临时值以及在调用函数时传递参数。需要在函数调用中生存的变量被保存到堆栈中。

BEAM 是一个线程代码解释器。每条指令都是一个指向可执行 C 代码的单词,这使得指令调度非常快。

这曾经是正确的,但从 R6B 开始,BEAM 编译器可以看到一个变量没有被使用。

类似地,对源代码级别的微不足道的转换,比如将一个 case 语句转换为函数顶层的子句,很少会对生成的代码产生任何影响。

将 Erlang 代码重写为 NIF 以使其更快,应该被视为最后的手段。它只保证危险,但不保证能加速程序。

在每个 NIF 调用中做太多工作会 降低 VM 的响应能力。做太少的工作可能意味着在 NIF 中更快处理的收益被调用 NIF 和检查参数的开销所抵消。

在编写 NIF 之前,请务必阅读有关 长时间运行的 NIF 的内容。