且听疯吟 如此生活三十年
Efficiency Guide:关于 Erlang 效率的 8 个迷思
Premature optimization is the root of all evil. -- D.E. Knuth
过早的优化是万恶之源. -- D.E. Knuth

渣翻译,且作读书笔记 :)
原文:The Eight Myths of Erlang Performance


迷思 1:Funs 很慢

funs 曾经是比较慢,不,应该说是特别慢,甚至比那个 apply/3 还慢。因为以前我们都是用一堆的语法糖啦,普通的元组啦,还有 apply/3 啦加上我们的奇技淫巧来实现的。
不过这些都是老黄历了,在 R6B 我们给了它专有的数据类型,并且在 R7B 做了更牛逼的优化,现在它的调用消耗已经降低到
本地调用和 apply/3 之间了。

不靠谱的 Note :

  1. 这里的 funs 应该是包含了 anonymous function 和 F = fun FunctionName/Arity; F(Arg1, Arg2, ..., Argn) 这些调用方式,在调用效率上这两种应该是基本等价的
  2. 在 OTP R5 和更早之前的版本中,funs 使用元组来表示,在之后的版本中有了专有的数据结构和优化1
  3. 对于参数个数已知的函数,M:F([Arg1, Arg2, … , Argn]) 的调用优于 apply
  4. 使用 apply 调用的函数编译器无法优化,同时许多分析工具也无法分析其细节2

迷思 2:列表推导很慢

列表推导曾经是用 funs 来实现的,当然参照第一点,你懂的。
现在编译器把列表推导重写成一个普通的递归函数。当然,写成尾递归加反转的方式看起来会更快。真的吗?我们下回分解。

迷思 3:尾递归函数快于普通递归函数

我们都知道普通递归函数会在堆栈上留下一堆可能不再使用的数据,而垃圾回收器没有这么聪明,它会不停的拷贝这些数据。而尾递归中这些数据会很快消亡。

在 R7B 之前这么说没错。在 R7B 中,编译器会把从来不会被用到的数据引用重写成空列表,这样垃圾回器收器就不需要做无用功了。

当然即使是在优化过后,尾递归函数还是在大多数时候还是比一个普通递归要快。
实际上这跟每次递归调用中消耗的堆栈空间有关。一般来说普通递归每次消耗的堆栈空间比尾递归所分配的堆空间要多。更多的内存消耗意味着更多的垃圾回收,以及更多的堆栈遍历。

在 R12B 以及后续版本中我们又进行了优化,减少了递归调用需要的堆栈空间,普通递归和一个尾递归需要得内存空间在大部分情况下都差不多了。lists:map/2lists:filter/2,列表推导,和其他一些普通递归函数占用的内存空间和尾递归实现一样了。

回到我们的老问题,哪一个更快?

看情况。在 Solaris/Sparc 上,普通递归看起来稍微快那么一点点,即使对于那些有很多元素的列表。在 x86 架构上,尾递归要比普通递归快近 30%。

剩下的就是个人口味的选择了。如果需要追求极限的速度,就必须要自己衡量了。你再也不能拍着胸脯说尾递归在所有情况下都快于普通递归。

注意:一个尾递归函数,如果最后的结果不需要 lists:reverse/1,显然比一个递归函数要快,因为尾递归函数不需要构造数据项(例如,对列表求和函数)。

迷思 4:++ 总是不好的

++ 操作符的名声总是很糟糕。其实你们都误解了它。主要是跟下面这种写法有关:

  • 不要这样做
    naive_reverse([H|T]) ->
        naive_reverse(T)++[H];
    naive_reverse([]) ->
        [].
    
    这是效率最低的反转列表的方式。因为 ++ 操作符原理是拷贝它左边的列表,结果就是一遍又一遍的拷贝……最终导致 n2 的复杂度。
  • 像这样的使用是没问题的
    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],那这种写法就毫无疑问的胜出了)

迷思 5:字符串很慢

某种程度上来说,操作不当会导致字符串处理起来很慢。
在 Erlang 中,需要在字符串的使用方式上多注意。
另外如果打算用正则表达式,用 re 模块,不要用废弃的 regexp 模块。

不靠谱的 Note :

  1. 实际上确实是「慢」
  2. 这个「慢」倒并不一定体现在运行效率上,不如说是增加了编码的麻烦。
  3. 由于字符串作为列表处理,但字符串是无法避免大量的拼接、裁剪、反转等操作的,要避免额外的列表拷贝,而且在使用的时候必须很小心。

迷思 6:修复一个 Dets 文件很慢

修复时间和 Dets 文件中的记录(records)数量依然成正比,但是过去修复 Dets 非常非常慢,而现在已经改进了。

迷思 7:BEAM 是一个基于堆栈的字节码虚拟机(因此很慢)

实际上 BEAM 是一个 threaded-code 解释器。每条指令直接指向可执行 C 代码,指令调度是非常快的。

迷思 8:用 _ 来表示不使用的变量可以让程序更快

好吧,在古老的某个时候它是对的。
但是从 R6B 版本开始,BEAM 编译器已经足够聪明到发现哪些变量是没有使用的了。