3 常见的警告
本节罗列出几个需要注意的模块和NIF,不仅仅是从性能的角度。
3.1 定时器模块
使用 erlang:send_after/3 和 erlang:start_timer/3 接口创建定时器比使用 STDLIB 里的 timer 模块提供的timer接口创建定时器更加高效。timer模块使用一个单独的进程管理定时器。如果许多进程频繁地创建和取消计时器(特别是在使用SMP仿真器时),则该定时器管理进程很容易过载。
在 timer 模块有一些函数(例如 timer:tc/3 和 timer:sleep/1)不会管理定时器,也不调用定时服务器进程,因此它们是无害的。
3.2 list_to_atom/1
原子是不会进行垃圾回收的,一旦被创建,就永远不会被删除。如果原子的数量达到限制数(默认是 1048576),仿真器则会终止。
因此,在一个持续运转的系统中,将任意输入的字符串参数转换成为原子的操作是十分危险的。如果只允许 “已经定义的” 原子作为输入,list_to_existing_atom/1 可以用来防止 “阻断式服务攻击” 。 (即所有允许的原子都已经提前创建好,例如加载一个模块,并在该模块中使用所有的原子)
如下代码,使用 list_to_atom/1 构造一个原子作为参数传递给 apply/3。这样的代价是十分昂贵的,不推荐在严格要求时间的代码中使用。
apply(list_to_atom("some_prefix"++Var), foo, Args)
3.3 length/1
使用 length/1 计算列表长度所花费的时间是与列表的长度成正比的。但是使用 tuple_size/1, byte_size/1 和 bit_size/1 计算对应数据类型的长度只需要常量的时间。
一般来讲,因为 length/1 函数是使用C语言高效实现的,所以不需要担心它的运行速度。如果在时间要求严格的代码中,你可以尽量避免使用它。
在一些情景下,length/1 可以用匹配来替换。如下代码:
foo(L) when length(L) >= 3 ->
...
被重构成:
foo([_,_,_|_]=L) ->
...
有个细微的区别是如果 L 不是一个正确的列表,执行到 length(L) 会失败。但是第二个代码段中的匹配模式可以编写兼容代码接受一个不正确的列表。
3.4 setelement/3
setelement/3 会复制它所修改的元组。因为,在循环中使用 setelement/3 更新一个元组每次循环都会复制一次元组。
上述复制元组的操作存在例外情况。当编译器很清楚的知道,使用破坏原本元组的结构的方式和复制原本元组的方式来更新该元组会产生同样的效果的时候,settlement/3 的函数调用会被替换成破坏性的setelement指令。在如下代码中,第一次调用 setelement/3 会复制改元组和修改元组中第九个元素:
multiple_setelement(T0) ->
T1 = setelement(9, T0, bar),
T2 = setelement(7, T1, foobar),
setelement(5, T2, new_value).
第二和第三次调用则在适当的地方修改元组。
要使优化得以进行,必须符合以下条件:
- 索引必须是整形数字,不可以是变量或者表达式。
- 索引必须按照降序排列。
- 在多次调用 setelement/3 的代码中间不可以调用其他函数。
- 第一次调用 setelement/3 返回的元组必须只能被下一次调用 settlement/3 所使用。
如果代码不能像例子一样遵循上述条件,那么修改一个大元组最好的方法是将元组转换成为列表,修改列表,然后再转换回元组。
3.5 size/1
size/1 可以返回元组和二进制数的大小。
使用 BIFS tuple_size/1 和 byte_size/1 可以给编译器和运行时系统更多优化的机会。另一个好处是 BIFS 给 Dialyzer 提供更多关于类型的信息。
3.6 split_binary/2
通常来说,使用匹配的方式分离一个二进制数比调用 split_binary/2 更高效。另外,如果将按比特匹配的语法和 split_binary/2 混合使用会阻止一个比特语法匹配的优化。
建议
<<Bin1:Num/binary,Bin2/binary>> = Bin,
不建议
{Bin1,Bin2} = split_binary(Bin, Num)