3 常见注意事项
本节列出了几个需要注意的模块和 BIF,不仅从性能的角度来看。
3.1 Timer 模块
使用 erlang:send_after/3 和 erlang:start_timer/3 创建计时器,比使用 STDLIB 中的 timer 模块提供的计时器效率更高。
timer 模块使用一个单独的进程来管理计时器。在 OTP 25 之前,这种管理开销很大,而且随着计时器数量的增加而增加,尤其是当计时器很短时,计时器服务器进程很容易变得过载和无响应。在 OTP 25 中,通过删除大多数管理开销,对计时器模块进行了改进,从而消除了由此带来的性能损失。但是,计时器服务器仍然是一个单进程,并且在某些情况下可能会成为应用程序的瓶颈。
timer 模块中不管理计时器的函数(例如 timer:tc/3 或 timer:sleep/1)不会调用计时器服务器进程,因此是无害的。
3.2 意外复制和共享丢失
使用 fun 生成新进程时,可能会意外地将比预期更多的数据复制到该进程。例如
不要
accidental1(State) -> spawn(fun() -> io:format("~p\n", [State#state.info]) end).
fun 中的代码将从记录中提取一个元素并打印它。state 记录的其余部分未使用。但是,执行 spawn/1 函数时,整个记录将被复制到新创建的进程。
类似的问题也可能发生在映射中
不要
accidental2(State) -> spawn(fun() -> io:format("~p\n", [map_get(info, State)]) end).
在以下示例(实现 gen_server 行为的模块的一部分)中,创建的 fun 被发送到另一个进程
不要
handle_call(give_me_a_fun, _From, State) -> Fun = fun() -> State#state.size =:= 42 end, {reply, Fun, State}.
这种不必要的复制的糟糕程度取决于记录或映射的内容。
例如,如果 state 记录的初始化方式如下
init1() -> #state{data=lists:seq(1, 10000)}.
将有 10000 个元素的列表(或大约 20000 个堆词)复制到新创建的进程。
10000 个元素列表的不必要复制可能已经很糟糕了,但是如果 state 记录包含 **共享子项**,情况可能会更糟。以下是一个包含共享子项的简单示例
{SubTerm, SubTerm}
将项复制到另一个进程时,子项的共享将丢失,复制的项可能比原始项大很多倍。例如
init2() -> SharedSubTerms = lists:foldl(fun(_, A) -> [A|A] end, [0], lists:seq(1, 15)), #state{data=Shared}.
在调用 init2/0 的进程中,state 记录中 data 字段的大小将为 32 个堆词。当记录被复制到新创建的进程时,共享将丢失,复制的 data 字段的大小将为 131070 个堆词。有关 共享丢失 的更多详细信息,请参阅后面的部分。
为了避免这个问题,在 fun 之外,只提取实际使用的记录字段
做
fixed_accidental1(State) -> Info = State#state.info, spawn(fun() -> io:format("~p\n", [Info]) end).
类似地,在 fun 之外,只提取实际使用的映射元素
做
fixed_accidental2(State) -> Info = map_get(info, State), spawn(fun() -> io:format("~p\n", [Info]) end).
3.3 list_to_atom/1
原子不会被垃圾回收。原子一旦创建,便不会被删除。如果达到原子数量的限制(默认值为 1,048,576),模拟器将终止。
因此,将任意输入字符串转换为原子在持续运行的系统中可能很危险。如果只允许某些定义明确的原子作为输入,则可以使用 list_to_existing_atom/1 来防止拒绝服务攻击。(所有允许的原子都必须在之前创建,例如,通过简单地在模块中使用它们并加载该模块。)
使用 list_to_atom/1 来构造传递给 apply/3 的原子,如下所示,代价很高,不建议在时间关键代码中使用
apply(list_to_atom("some_prefix"++Var), foo, Args)
3.4 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.5 setelement/3
setelement/3 会复制它修改的元组。因此,使用 setelement/3 在循环中更新元组,每次都会创建一个新的元组副本。
元组被复制的规则有一个例外。如果编译器可以清楚地看到,破坏性地更新元组会与复制元组产生相同的结果,则对 setelement/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 的调用之间,不得调用其他函数。
- 从一个 setelement/3 调用返回的元组只能在随后的 setelement/3 调用中使用。
如果代码不能像 multiple_setelement/1 示例那样构建,那么修改大型元组中多个元素的最佳方法是将元组转换为列表,修改列表,然后将其转换回元组。
3.6 size/1
size/1 返回元组和二进制的大小。
使用 BIF tuple_size/1 和 byte_size/1 可以为编译器和运行时系统提供更多优化机会。另一个好处是,BIF 可以为 Dialyzer 提供更多类型信息。
3.7 split_binary/2
使用匹配而不是调用 split_binary/2 函数来分割二进制通常更高效。此外,混合使用位语法匹配和 split_binary/2 会阻止一些位语法匹配的优化。
做
<<Bin1:Num/binary,Bin2/binary>> = Bin,
不要
{Bin1,Bin2} = split_binary(Bin, Num)