ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Erlang/OTP 进程性能优化完全指南:从进程创建到消息传递的底层原理与实战

Erlang/OTP 进程性能优化完全指南:从进程创建到消息传递的底层原理与实战 编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载本文基于 Erlang/OTP 官方 Efficiency Guide 中的进程专题system/doc/efficiency_guide/eff_guide_processes.md结合 OTP 编译器与运行时系统源码深入解析 Erlang 进程的轻量级本质、内存模型、消息传递与选择性接收优化、字面量池与共享丢失机制以及 SMP 多核利用的前提条件。读完本文你将掌握如何测量进程内存占用、写出正确的尾递归主循环、利用monitor/2触发 receive 优化、借助recv_opt_info验证优化是否生效以及合理调整初始堆大小与字面量地址空间等实战技巧。一、Erlang 进程为何轻量与操作系统线程的本质差异Erlang 进程process是 BEAM 虚拟机调度的基本并发单元。与操作系统线程和进程相比Erlang 进程在创建成本、上下文切换成本和内存占用上都极其轻量这正是 Erlang 能够支撑成千上万甚至百万级并发进程的根本原因。从源码结构看Erlang 进程完全由虚拟机管理不依赖操作系统调度器。调度器scheduler在 erts/emulator/beam 目录下实现每个调度器线程在运行队列run queue上以抢占式方式调度 Erlang 进程进程间的切换不需要陷入操作系统内核。测量一个新进程的内存占用文档给出了一个可复现的测量方法先 spawn 一个永远不会返回的进程receive after infinity - ok end再通过process_info/2读取其memory值最后除以wordsize机器字长64 位系统为 8 字节换算成字word为单位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 CtrlG to abort, type help(). for help) 1 Fun fun() - receive after infinity - ok end end. #Funerl_eval.43.39164016 2 {_,Bytes} process_info(spawn(Fun), memory). {memory,2616} 3 Bytes div erlang:system_info(wordsize). 327关键结论一个新建的 Erlang 进程仅占用 327 个字words的内存。在 64 位系统上约合 2616 字节远小于一个典型的操作系统线程通常以 MB 计。其中233 个字是堆区域heap area进程栈也包含在堆区域内。堆由垃圾收集器GC按需增长。也就是说233 字只是起点进程用到更多数据时 GC 会自动扩展堆。这个数字对应 efficiency_guide.erl 这类可编译运行示例中的进程基础开销是理解百万进程可行性的重要依据。二、主循环必须尾递归栈溢出的隐形杀手Erlang 进程的典型形态是接收消息 → 处理 → 继续循环的 actor 模型。文档明确强调进程的主外层循环必须写成尾递归形式否则每一次循环调用都会在栈上压入返回地址栈会持续增长直到进程终止。反例DO NOTloop() - 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()结尾并永远递归永远不会返回到io:format。但每一次递归调用仍会压入一个返回地址随着消息不断涌入进程栈会无限增长。正确写法DOloop() - receive {sys, Msg} - handle_sys_msg(Msg), loop(); {From, Msg} - Reply handle_msg(Msg), From ! Reply, loop() end.唯一的区别是去掉循环体后面的io:format/2调用。尾递归调用在编译后会被转换为跳转指令不占用新的栈帧因此循环可以永远进行下去而内存占用保持恒定。这一点在编写任何长期运行的服务进程如 gen_server 的回调循环时都是必须遵守的铁律。三、初始堆大小Initial Heap Size何时调整 min_heap_size默认 233 字是刻意保守的选择默认初始堆 233 字相当保守这是为了支持拥有数十万乃至上百万进程的 Erlang 系统——堆越小单位内存能容纳的进程越多。GC 会根据需要增长和收缩堆。两种调整手段在进程数量相对较少的系统中性能可能通过提高最小堆大小得到改善全局VM 级使用erl的h选项即 erl 命令行参考 中描述的h size最小堆大小设定逐个进程进程级使用spawn_opt/4的min_heap_size选项为特定进程单独指定最小堆大小。例如在代码中spawn_opt(fun() - heavy_short_task() end, [{min_heap_size, 10000}]).双重收益提高最小堆大小的收益来自两个方面GC 的堆增长是分步进行的默认情况下堆需要多大就逐步长多大每次扩容都有成本如果 spawn 时就一次性建立足够大的堆可以省去这些逐步扩容的开销阻止堆收缩当堆远大于其上的数据量时GC 会收缩堆设置最小堆大小可以防止这种收缩反复发生。必须注意的警告警告运行时系统可能会消耗更多内存同时由于垃圾收集频率降低巨大二进制huge binaries可能被保留更长时间。 该优化必须在充分测量之后再进行不可盲目使用。文档还给出一个典型的使用模式在进程数量众多的系统中可以把运行时间较短的纯计算任务spawn 到带有较高最小堆大小的新进程中。任务完成后把结果发给其他进程并自行终止。如果最小堆大小计算得当该进程可能根本不需要做任何垃圾收集——这是消除 GC 停顿的最彻底手段。从源码佐证看min_heap_size是进程的固有属性可通过erlang:system_info/1的min_heap_size项查询见 erlang_system_info.md。四、发送消息复制语义与例外Erlang 消息传递遵循复制语义copy semantics进程间发送消息时消息中的数据结构被整体复制到接收进程的堆上接收者绝不会与发送者共享可变数据这正是 Erlang 并发安全性的基础。两个例外但并非所有数据都会被复制同节点内有两条例外引用计数二进制refc binaries参见 binaryhandling.md 的 Refc Binaries 一节。Refc binary 分为两部分进程堆上的 ProcBin 对象以及堆外存储的二进制对象本体。二进制对象可以被任意数量进程的 ProcBin 引用对象内含引用计数器最后一个引用消失时才被回收。因此发送大二进制时消息中只复制 ProcBin 头二进制本体零拷贝同节点上的字面量literals见后文字面量池一节字面量发送时也不复制。跨节点发送外部格式编码当消息发送到另一个 Erlang 节点上的进程时流程完全不同消息先被编码为 Erlang External Format即分布式 Erlang 的二进制线格式通过 TCP/IP socket 传输接收节点解码消息再分发到正确的目标进程。这意味着跨节点消息传递的开销远高于同节点且涉及完整的序列化/反序列化。设计分布式系统时应当意识到同节点复制成本与跨节点编码传输成本的量级差异。五、接收消息与选择性接收优化从 O(n) 到 O(1)简单接收总是很快从消息队列中取出消息的成本取决于receive表达式的复杂程度。匹配任意消息的简单接收直接取出队列头部的第一条消息成本极低DOreceive Message - handle_msg(Message) end.选择性接收昂贵的队列扫描但实际代码中我们往往只匹配期望的消息比如带标签的{Tag, Message}receive {Tag, Message} - handle_msg(Message) end.这种写法很便利但代价是必须遍历整个消息队列直到找到匹配项。对于消息队列很长的进程例如集中式请求汇聚点这是非常昂贵的操作——每条不匹配的消息都要被检查一遍复杂度为 O(队列长度)。编译器优化用 reference 标记队列位置针对最常见的发请求、等回复模式编译器有一种重要优化只要receive的所有子句都匹配某个新创建的 reference全局唯一标识虚拟机就只搜索该 reference 创建之后到达的消息从而把扫描范围从整个队列缩小到调用之后的消息。DOMRef 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创建的 reference 在调用之前不可能存在因为它是全局唯一的而receive只匹配包含该 reference 的消息因此可以告诉模拟器只搜索monitor/2调用之后到达的消息。顺带一提这个写法同时用 DOWN 消息处理了目标进程提前终止的情况是请求-应答模式的推荐范式。recv_opt_info验证优化是否生效但更复杂的代码中优化并不总是能自动生效。例如把 reference 作为参数传入另一个函数、在receive之前对 reference 做复杂处理等场景编译器可能无法证明其唯一性。为此编译器提供recv_opt_info选项让编译器打印每条 receive 的优化分析信息。使用方法有两种erlc recv_opt_info Mod.erl或通过环境变量传递推荐因为该选项产生的信息无法全部消除不适合永久写入 Makefileexport ERL_COMPILER_OPTIONSrecv_opt_info警告输出形如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将警告以注释形式标注回代码可以更清楚地理解每种情况示例取自 efficiency_guide.erl%% 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.注意最后cross_function_receive/0的例子make_ref()创建的引用被作为函数参数传入编译器仍能跨函数追踪到该引用在函数参数 1 中这一事实并完成优化。源码级实现佐证receive 优化由编译器在 SSA 中间表示上实现核心代码位于 lib/compiler/src/beam_ssa_recv.erl。其流程从源码结构看为scan/1扫描模块中所有peek_message指令即 receive 的底层 IR 指令收集接收候选recv_candidates与引用候选ref_candidatesplan/1在模块级控制流图上规划 marker标记队列位置的创建、使用与清除optimize/4根据规划改写 IR当recv_opt_info选项开启时collect_opt_info/1收集并格式化上述诊断信息见 beam_ssa_recv.erl。recv_opt_info选项的官方说明见 compile.erl编译器会发出关于 receive 优化情况的信息性警告。六、字面量池Literal Pool常量术语的存放与复制规则常量术语不重复构建Erlang 编译器会把常量术语literals如元组、列表等编译期常量放进字面量池每个已加载模块拥有自己独立的池。例如下面的函数不会在每次调用时构建那个 12 元组——它位于模块的字面量池中只存在一份DOdays_in_month(M) - element(M, {31,28,31,30,31,30,31,31,30,31,30,31}).对应的完整实现见 efficiency_guide.erl。复制规则Ets 复制、进程间不复制插入 Ets 表时会被复制如果一个字面量或包含字面量的术语被插入 Ets 表它会被复制。原因很直接包含该字面量的模块未来可能被卸载unloadEts 表必须持有独立的数据。发送给其他进程时不复制字面量作为消息发送给另一个进程时是不复制的。模块卸载时当持有某字面量的模块被卸载所有持有该字面量引用的进程的堆上都会被复制一份该字面量。全局字面量池persistent_term除各模块的私有池外还存在一个全局字面量池由m:persistent_term模块管理。persistent_term适合存放低频更新、高频读取的全局常量如配置数据读取成本极低但更新代价高——这与进程私有字面量池是互补的两套机制。虚拟地址空间预留MIscs默认情况下所有字面量池BEAM 代码中的字面量 persistent terms会预留1 GB 的虚拟地址空间。该预留量可通过MIscs选项调整其完整定义见 erts_alloc.md 的 literal_alloc 特殊旗标MIscs size in MBliteral_alloc超级载体super carrier大小MB即 64 位架构上为 Erlang 代码中的字面量术语预留的虚拟地址空间大小。默认 1024即 1 GB通常足够。该旗标在 32 位架构上被忽略。例如把字面量预留空间提升到 2 GB2048 MBerl MIscs 2048注意这是虚拟地址空间预留并非立即占用等量物理内存因此对大多数系统 1 GB 默认值已经充足仅在极端场景海量 persistent_term 或超大代码库下才需要调高。七、共享丢失Loss of Sharing当术语的共享结构被展平共享子项与三种丢失场景Erlang 术语可以包含共享子项shared subterms例如{SubTerm, SubTerm}中两个元素指向同一份数据。共享是内存优化手段但在以下三种情况下共享不会被保留术语被发送到另一个进程时术语作为spawn调用的初始进程参数传递时术语被存储进 Ets 表时。这是一项刻意的优化简化运行时实现而且绝大多数应用并不会发送带共享子项的消息因此几乎无感。但在构造超大共享结构的场景如共享前缀的 DAG 结构中一旦数据穿过上述边界内存占用可能急剧膨胀。用 kilo_byte/1 验证共享与展平efficiency_guide.erl 中的kilo_byte/1通过反复[Acc|Acc]构造出一个深度共享的列表kilo_byte() - kilo_byte(10, [42]). kilo_byte(0, Acc) - Acc; kilo_byte(N, Acc) - kilo_byte(N-1, [Acc|Acc]).在 shell 中验证1 byte_size(list_to_binary(efficiency_guide:kilo_byte())). 1024list_to_binary/1把深层列表展平成 1024 字节的二进制。用erts_debug:size/1查看共享结构实际占用2 erts_debug:size(efficiency_guide:kilo_byte()). 22仅 22 个字——因为共享整个 1024 字节结构在堆上只存了一份骨干。而erts_debug:flat_size/1计算忽略共享时的尺寸即数据被发送到其他进程或存入 Ets 表后的实际大小3 erts_debug:flat_size(efficiency_guide:kilo_byte()). 40944094 个字——将近 200 倍的差距这就是共享丢失的代价。穿过 Ets 表验证共享丢失把数据插入 Ets 表后再取回size/1与flat_size/1返回相同值说明共享已被展平4 T ets:new(tab, []). #Ref0.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实验性选项--enable-sharing-preserving如果想在复制术语时保留共享可以构建一个实验性的运行时系统给configure脚本传入--enable-sharing-preserving选项。注意该选项会带来运行时开销复制时必须遍历并维护共享图且是实验特性生产环境需谨慎评估。八、SMP 运行时系统多核利用的前提条件Erlang 运行时系统会自动利用多核/多 CPU 计算机运行多个 Erlang 调度器线程典型配置为线程数等于核心数。但关键前提是要从多核获益你的应用在大多数时间必须同时有多个可运行的 Erlang 进程。否则无论有几个核心模拟器同一时刻依然只能运行一个 Erlang 进程——多余的核处于空闲状态。文档特别提醒了一个常见误区看似并发的基准测试往往是顺序执行的。例如著名的 EStone 基准测试完全是顺序的最常见的ring benchmark环形消息传递基准实现同样如此——通常只有一个进程处于活动状态其余进程都在receive中等待。这类基准测不出 SMP 的并行能力测的是消息传递延迟。因此评估并行性能时应设计真正让多个进程同时可运行的负载如多个互不依赖的计算任务分配到不同进程并在多核机器上对比单调度器S 1与多调度器下的吞吐差异才能反映 SMP 调度的真实收益。九、要点速查主题核心结论实操手段进程开销新进程 327 字堆含栈 233 字process_info(spawn(F), memory) div system_info(wordsize)主循环必须尾递归否则栈无限增长删除循环体后不可达代码初始堆默认 233 字保守值可按需调大h或spawn_opt/4的min_heap_size消息复制全量复制refc binary 与同节点字面量除外大二进制天然零拷贝跨节点消息编码为 External Format 经 TCP/IP 传输避免高频跨节点小消息选择性接收默认扫描整个队列reference 标记可裁剪搜索范围用monitor/2或make_ref/0引用匹配recv_opt_info验证字面量池每模块一个池Ets 复制、进程间不复制1 GB 虚拟地址预留MIscs MB调整预留共享丢失跨进程/跨 Ets 后共享被展平erts_debug:size/1与flat_size/1对比评估SMP需同时存在多个可运行进程才能利用多核避免顺序化的假并发基准十、进一步阅读二进制处理与 refc/heap 二进制细节system/doc/efficiency_guide/binaryhandling.md消息在节点间传输的线格式erts/doc/references/erl_ext_dist.md分配器与MIscs等内存旗标erts/doc/references/erts_alloc.md编译器 receive 优化实现lib/compiler/src/beam_ssa_recv.erl文中所用示例代码的完整实现system/doc/efficiency_guide/efficiency_guide.erl赞分享编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载相关推荐Erlang/OTP 并发编程入门从进程、消息传递到分布式 ping-pong 与 messenger 实战Erlang/OTP 并发编程入门从进程、消息传递到分布式 ping pong 与 messenger 实战 本篇指南基于 Erlang/OTP 官方入门教程编程语言语言运行时标准库编译器并发编程Erlang/OTP 效率指南Efficiency Guide精讲从列表、二进制到进程与映射的性能优化实战Erlang/OTP 效率指南Efficiency Guide精讲从列表、二进制到进程与映射的性能优化实战 导读本文基于 Erlang/OTP 官方 E编程语言语言运行时标准库编译器并发编程Erlang/OTP 二进制构建与匹配优化完全指南从内部实现到编译优化Erlang/OTP 二进制构建与匹配优化完全指南从内部实现到编译优化 本文基于 Erlang/OTP 官方效率指南Efficiency Guide中的编程语言语言运行时标准库编译器并发编程上一篇CoreMLTools模型转换全解析从TensorFlow到Core ML的完整流程下一篇Numba Literal 类型深入解析用编译期常量实现类型稳定的特化编译创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表