2 Erlang 性能的七个神话
一些真理似乎比其最佳使用期限存活的时间更长,也许是因为 "信息" 人与人之间传播的速度比一篇单一的发布说明快,例如,说主体递归调用已经变得更快了。
本节试图消除已经成为神话的旧真理(或半真理)。
2.1 神话:尾递归函数比递归函数快得多
根据神话,使用一个尾递归函数,它反向构建一个列表,然后调用 lists:reverse/1,比一个主体递归函数更快,该函数按正确顺序构建列表;原因是主体递归函数比尾递归函数使用更多内存。
在 R12B 之前,这在一定程度上是正确的。在 R7B 之前,情况更加如此。今天,情况并非如此。一个主体递归函数通常使用与尾递归函数相同的内存量。通常无法预测尾递归版本还是主体递归版本更快。因此,使用使你的代码更简洁的版本(提示:通常是主体递归版本)。
有关尾递归和主体递归的更深入讨论,请参见 Erlang 的尾递归不是灵丹妙药。
一个不需要在最后反转列表的尾递归函数比一个主体递归函数更快,尾递归函数也不构造任何项(例如,一个函数,它对列表中的所有整数求和)。
2.2 神话:操作符 "++" 总是很糟糕
++ 操作符多少有些名不副实地获得了不好的名声。这可能与以下代码有关,这是反转列表最没有效率的方式
不要
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],它会更有效。)
2.3 神话:字符串很慢
如果处理不当,字符串处理可能会很慢。在 Erlang 中,你需要更多地考虑字符串的使用方式,并选择合适的表示形式。如果你使用正则表达式,请使用 STDLIB 中的 re 模块,而不是过时的 regexp 模块。
2.4 神话:修复 Dets 文件非常慢
修复时间仍然与文件中的记录数量成正比,但 Dets 的修复在过去曾经慢得多。Dets 已经进行了大规模的重写和改进。
2.5 神话:BEAM 是一个基于栈的字节码虚拟机(因此很慢)
BEAM 是一个基于寄存器的虚拟机。它有 1024 个虚拟寄存器,用于保存临时值以及在调用函数时传递参数。需要在函数调用中生存的变量被保存到堆栈中。
BEAM 是一个线程代码解释器。每条指令都是一个指向可执行 C 代码的单词,这使得指令调度非常快。
2.6 神话:当一个变量未被使用时,使用 "_" 可以加速你的程序
这曾经是正确的,但从 R6B 开始,BEAM 编译器可以看到一个变量没有被使用。
类似地,对源代码级别的微不足道的转换,比如将一个 case 语句转换为函数顶层的子句,很少会对生成的代码产生任何影响。
2.7 神话:NIF 总是能加速你的程序
将 Erlang 代码重写为 NIF 以使其更快,应该被视为最后的手段。它只保证危险,但不保证能加速程序。
在每个 NIF 调用中做太多工作会 降低 VM 的响应能力。做太少的工作可能意味着在 NIF 中更快处理的收益被调用 NIF 和检查参数的开销所抵消。
在编写 NIF 之前,请务必阅读有关 长时间运行的 NIF 的内容。