9 进程
9.1 创建 Erlang 进程
与操作系统中的线程和进程相比,Erlang 进程是轻量级的。
新生成的 Erlang 进程使用 326 个字的内存。大小可以通过以下方式找到
Erlang/OTP 24 [erts-12.0] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] Eshell V5.6 (abort with ^G) 1> Fun = fun() -> receive after infinity -> ok end end. #Fun<...> 2> {_,Bytes} = process_info(spawn(Fun), memory). {memory,1232} 3> Bytes div erlang:system_info(wordsize). 309
大小包括堆区域的 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 选项来提高性能。
收益有两方面
- 尽管垃圾收集器会扩展堆,但它是逐步扩展的,这比在进程生成时直接建立一个更大的堆更昂贵。
- 如果堆远大于存储在其中的数据量,垃圾收集器也可以收缩堆;设置最小堆大小可以防止这种情况发生。
模拟器可能使用更多内存,并且由于垃圾收集发生的频率较低,因此可以更长时间地保留大型二进制文件。
在具有许多进程的系统中,运行时间较短的计算任务可以生成一个具有更高最小堆大小的新进程。当进程完成时,它会将计算结果发送到另一个进程并终止。如果最小堆大小计算正确,进程可能根本不需要进行任何垃圾收集。此优化不能在没有适当测量的情况下进行尝试。
9.2 发送消息
在 Erlang 进程之间发送的消息中的所有数据都会被复制,除了位于同一 Erlang 节点上的 refc 二进制文件 和 文字。
当消息发送到另一个 Erlang 节点上的进程时,它首先会被编码为 Erlang 外部格式,然后通过 TCP/IP 套接字发送。接收 Erlang 节点会解码消息并将消息分发到正确的进程。
9.3 接收消息
接收消息的成本取决于 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.
9.4 文字池
常量 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
9.5 共享丢失
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 选项。
9.6 SMP 模拟器
模拟器通过运行多个 Erlang 调度程序线程(通常与内核数相同)来利用多核或多 CPU 计算机。
要从多核计算机中获得性能提升,您的应用程序必须在大多数时间具有多个可运行的 Erlang 进程。否则,Erlang 模拟器一次只能运行一个 Erlang 进程。
似乎是并发的基准测试通常是顺序的。例如,estone 基准测试完全是顺序的。同样,"环形基准测试" 的最常见实现也是如此;通常只有一个进程处于活动状态,而其他进程则在 receive 语句中等待。