查看源代码 进程
创建 Erlang 进程
与操作系统中的线程和进程相比,Erlang 进程是轻量级的。
一个新生成的 Erlang 进程使用 327 个字长的内存。大小可以通过以下方式找到
Erlang/OTP 27 [erts-14.2.3] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Eshell V14.2.3 (press Ctrl+G to abort, type help(). for help)
1> Fun = fun() -> receive after infinity -> ok end end.
#Fun<erl_eval.43.39164016>
2> {_,Bytes} = process_info(spawn(Fun), memory).
{memory,2616}
3> Bytes div erlang:system_info(wordsize).
327
该大小包括堆区域的 233 个字长(包括堆栈)。垃圾收集器会根据需要增加堆的大小。
进程的主要(外部)循环必须是尾递归的。否则,堆栈会增长直到进程终止。
不要这样做
loop() ->
receive
{sys, Msg} ->
handle_sys_msg(Msg),
loop();
{From, Msg} ->
Reply = handle_msg(Msg),
From ! Reply,
loop()
end,
io:format("Message is processed~n", []).
对 io:format/2
的调用永远不会被执行,但是每次递归调用 loop/0
时,仍然会将返回地址压入堆栈。正确的尾递归版本的函数如下所示
这样做
loop() ->
receive
{sys, Msg} ->
handle_sys_msg(Msg),
loop();
{From, Msg} ->
Reply = handle_msg(Msg),
From ! Reply,
loop()
end.
初始堆大小
默认的 233 字长的初始堆大小是相当保守的,目的是支持拥有数十万甚至数百万进程的 Erlang 系统。垃圾收集器会根据需要增大和缩小堆。
在进程相对较少的系统中,通过使用 erl 的 +h
选项,或者通过在每个进程的基础上使用 spawn_opt/4 的 min_heap_size
选项,增加最小堆大小可能会提高性能。
这样做的好处有两个
- 虽然垃圾收集器会增大堆,但它是逐步增大的,这比在进程生成时直接建立更大的堆要昂贵。
- 如果堆比存储在其中的数据量大得多,垃圾收集器也可以缩小堆;设置最小堆大小可以防止这种情况发生。
警告
运行时系统可能会使用更多内存,并且由于垃圾回收的频率降低,大型二进制文件可以保留更长时间。
如果没有进行适当的测量,请不要尝试此优化。
在具有许多进程的系统中,可以生成一个具有较高最小堆大小的新进程来执行短时间的计算任务。当进程完成时,它会将计算结果发送给另一个进程并终止。如果正确计算最小堆大小,则该进程可能根本不需要进行任何垃圾回收。
发送消息
在 Erlang 进程之间发送的消息中的所有数据都会被复制,除了在同一 Erlang 节点上的 refc 二进制文件 和 字面量。
当消息发送到另一个 Erlang 节点上的进程时,它首先被编码为 Erlang 外部格式,然后通过 TCP/IP 套接字发送。接收 Erlang 节点解码消息并将其分发到正确的进程。
接收消息
接收消息的成本取决于 receive
表达式的复杂程度。一个匹配任何消息的简单表达式非常便宜,因为它检索消息队列中的第一条消息
这样做
receive
Message -> handle_msg(Message)
end.
但是,这并不总是方便的:我们可以收到一条我们目前不知道如何处理的消息,因此通常只匹配我们期望的消息
receive
{Tag, Message} -> handle_msg(Message)
end.
虽然这很方便,但这意味着必须搜索整个消息队列,直到找到匹配的消息。这对于具有长消息队列的进程来说非常昂贵,因此对于发送请求并在不久后等待响应的常见情况,有一种优化
这样做
MRef = monitor(process, Process),
Process ! {self(), MRef, Request},
receive
{MRef, Reply} ->
erlang:demonitor(MRef, [flush]),
handle_reply(Reply);
{'DOWN', MRef, _, _, Reason} ->
handle_error(Reason)
end.
由于编译器知道 monitor/2
创建的引用在调用之前不可能存在(因为它是一个全局唯一标识符),并且 receive
只匹配包含该引用的消息,因此它会告诉模拟器仅搜索在调用 monitor/2
之后到达的消息。
以上是一个简单的示例,其中一个保证会进行优化,但是更复杂的代码呢?
选项 recv_opt_info
使用 recv_opt_info
选项来让编译器打印有关接收优化的信息。它可以传递给编译器或 erlc
erlc +recv_opt_info Mod.erl
或者通过环境变量传递
export ERL_COMPILER_OPTIONS=recv_opt_info
请注意,recv_opt_info
并非旨在成为添加到您的 Makefile
的永久选项,因为它生成的所有消息都无法消除。因此,在大多数情况下,通过环境传递选项是最实用的方法。
警告如下所示
efficiency_guide.erl:194: Warning: INFO: receive matches any message, this is always fast
efficiency_guide.erl:200: Warning: NOT OPTIMIZED: all clauses do not match a suitable reference
efficiency_guide.erl:206: Warning: OPTIMIZED: reference used to mark a message queue position
efficiency_guide.erl:208: Warning: OPTIMIZED: all clauses match reference created by monitor/2 at efficiency_guide.erl:206
efficiency_guide.erl:219: Warning: INFO: passing reference created by make_ref/0 at efficiency_guide.erl:218
efficiency_guide.erl:222: Warning: OPTIMIZED: all clauses match reference in function parameter 1
为了更清楚地说明警告引用的确切代码,以下示例中的警告将作为注释插入到它们引用的子句之后,例如
%% DO
simple_receive() ->
%% efficiency_guide.erl:194: Warning: INFO: not a selective receive, this is always fast
receive
Message -> handle_msg(Message)
end.
%% DO NOT, unless Tag is known to be a suitable reference: see
%% cross_function_receive/0 further down.
selective_receive(Tag, Message) ->
%% efficiency_guide.erl:200: Warning: NOT OPTIMIZED: all clauses do not match a suitable reference
receive
{Tag, Message} -> handle_msg(Message)
end.
%% DO
optimized_receive(Process, Request) ->
%% efficiency_guide.erl:206: Warning: OPTIMIZED: reference used to mark a message queue position
MRef = monitor(process, Process),
Process ! {self(), MRef, Request},
%% efficiency_guide.erl:208: Warning: OPTIMIZED: matches reference created by monitor/2 at efficiency_guide.erl:206
receive
{MRef, Reply} ->
erlang:demonitor(MRef, [flush]),
handle_reply(Reply);
{'DOWN', MRef, _, _, Reason} ->
handle_error(Reason)
end.
%% DO
cross_function_receive() ->
%% efficiency_guide.erl:218: Warning: OPTIMIZED: reference used to mark a message queue position
Ref = make_ref(),
%% efficiency_guide.erl:219: Warning: INFO: passing reference created by make_ref/0 at efficiency_guide.erl:218
cross_function_receive(Ref).
cross_function_receive(Ref) ->
%% efficiency_guide.erl:222: Warning: OPTIMIZED: all clauses match reference in function parameter 1
receive
{Ref, Message} -> handle_msg(Message)
end.
字面量池
常量 Erlang 项(以下称为字面量)保存在字面量池中;每个加载的模块都有自己的池。以下函数不会在每次调用时都构建元组(仅在下次运行垃圾收集器时将其丢弃),但该元组位于模块的字面量池中
这样做
days_in_month(M) ->
element(M, {31,28,31,30,31,30,31,31,30,31,30,31}).
如果将字面量或包含字面量的项插入到 Ets 表中,则会对其进行复制。原因是包含字面量的模块将来可能会被卸载。
当字面量发送到另一个进程时,它不会被复制。当卸载包含字面量的模块时,字面量将被复制到所有持有对该字面量引用的进程的堆中。
还存在一个全局字面量池,由 persistent_term
模块管理。
默认情况下,会为所有字面量池(在 BEAM 代码和持久化项中)保留 1 GB 的虚拟地址空间。可以通过在启动模拟器时使用 +MIscs 选项
来更改为字面量保留的虚拟地址空间量。
以下示例说明如何将为字面量保留的虚拟地址空间提高到 2 GB(2048 MB)
erl +MIscs 2048
失去共享
一个 Erlang 项可以有共享的子项。这是一个简单的示例
{SubTerm, SubTerm}
在以下情况下,共享的子项不会保留
- 当一个项发送到另一个进程时
- 当一个项作为
spawn
调用中的初始进程参数传递时 - 当一个项存储在 Ets 表中时
这是一种优化。大多数应用程序不会发送带有共享子项的消息。
以下示例说明如何创建共享的子项
kilo_byte() ->
kilo_byte(10, [42]).
kilo_byte(0, Acc) ->
Acc;
kilo_byte(N, Acc) ->
kilo_byte(N-1, [Acc|Acc]).
kilo_byte/1
创建一个深层列表。如果调用 list_to_binary/1
,则可以将深层列表转换为 1024 字节的二进制文件
1> byte_size(list_to_binary(efficiency_guide:kilo_byte())).
1024
使用 erts_debug:size/1
BIF,可以看到深层列表仅需要 22 个字长的堆空间
2> erts_debug:size(efficiency_guide:kilo_byte()).
22
使用 erts_debug:flat_size/1
BIF,可以计算出如果忽略共享,深层列表的大小。当它已发送到另一个进程或存储在 Ets 表中时,它将变为列表的大小
3> erts_debug:flat_size(efficiency_guide:kilo_byte()).
4094
可以验证,如果将数据插入到 Ets 表中,则共享将会丢失
4> T = ets:new(tab, []).
#Ref<0.1662103692.2407923716.214181>
5> ets:insert(T, {key,efficiency_guide:kilo_byte()}).
true
6> erts_debug:size(element(2, hd(ets:lookup(T, key)))).
4094
7> erts_debug:flat_size(element(2, hd(ets:lookup(T, key)))).
4094
当数据通过 Ets 表时,erts_debug:size/1
和 erts_debug:flat_size/1
返回相同的值。共享已丢失。
可以通过向 configure
脚本提供 --enable-sharing-preserving
选项来构建运行时系统的实验性变体,该变体在复制项时将保留共享。
SMP 运行时系统
Erlang 运行时系统通过运行多个 Erlang 调度程序线程(通常,线程数与核心数相同)来利用多核或多 CPU 计算机。
要从多核计算机获得性能,您的应用程序必须在大多数时候拥有多个可运行的 Erlang 进程。否则,Erlang 模拟器一次仍然只能运行一个 Erlang 进程。
看起来是并发的基准测试通常是顺序的。例如,EStone 基准测试完全是顺序的。最常见的“环形基准测试”的实现也是如此;通常一个进程处于活动状态,而其他进程则在 receive
语句中等待。