3  常見注意事項

3 常见注意事项

本节列出了几个需要注意的模块和 BIF,不仅从性能的角度来看。

使用 erlang:send_after/3erlang:start_timer/3 创建计时器,比使用 STDLIB 中的 timer 模块提供的计时器效率更高。

timer 模块使用一个单独的进程来管理计时器。在 OTP 25 之前,这种管理开销很大,而且随着计时器数量的增加而增加,尤其是当计时器很短时,计时器服务器进程很容易变得过载和无响应。在 OTP 25 中,通过删除大多数管理开销,对计时器模块进行了改进,从而消除了由此带来的性能损失。但是,计时器服务器仍然是一个单进程,并且在某些情况下可能会成为应用程序的瓶颈。

timer 模块中不管理计时器的函数(例如 timer:tc/3timer:sleep/1)不会调用计时器服务器进程,因此是无害的。

使用 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).

原子不会被垃圾回收。原子一旦创建,便不会被删除。如果达到原子数量的限制(默认值为 1,048,576),模拟器将终止。

因此,将任意输入字符串转换为原子在持续运行的系统中可能很危险。如果只允许某些定义明确的原子作为输入,则可以使用 list_to_existing_atom/1 来防止拒绝服务攻击。(所有允许的原子都必须在之前创建,例如,通过简单地在模块中使用它们并加载该模块。)

使用 list_to_atom/1 来构造传递给 apply/3 的原子,如下所示,代价很高,不建议在时间关键代码中使用

apply(list_to_atom("some_prefix"++Var), foo, Args)

计算列表长度的时间与列表的长度成正比,而 tuple_size/1byte_size/1bit_size/1 的执行时间都是恒定的。

通常,不必担心 length/1 的速度,因为它在 C 中得到了有效的实现。在时间关键代码中,如果输入列表可能很长,您可能希望避免使用它。

一些 length/1 的用法可以使用匹配来替换。例如,以下代码

foo(L) when length(L) >= 3 ->
    ...

可以改写为

foo([_,_,_|_]=L) ->
   ...

一个细微的差别是,如果 L 是一个不正确的列表,则 length(L) 会失败,而第二个代码片段中的模式接受不正确的列表。

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 示例那样构建,那么修改大型元组中多个元素的最佳方法是将元组转换为列表,修改列表,然后将其转换回元组。

size/1 返回元组和二进制的大小。

使用 BIF tuple_size/1byte_size/1 可以为编译器和运行时系统提供更多优化机会。另一个好处是,BIF 可以为 Dialyzer 提供更多类型信息。

使用匹配而不是调用 split_binary/2 函数来分割二进制通常更高效。此外,混合使用位语法匹配和 split_binary/2 会阻止一些位语法匹配的优化。

        <<Bin1:Num/binary,Bin2/binary>> = Bin,

不要

        {Bin1,Bin2} = split_binary(Bin, Num)