BEAM简介

2020-10-22 08:51:53

这篇文章是关于BEAM的简短入门读物,BEAM是在Erlang Runtime system(ERTS)中执行用户代码的虚拟机。它的目的是帮助那些BEAM新手关注OTP24中即将发布的关于JIT的一系列帖子,将实现细节留到后面。

BEAM经常与ERTS混淆,区分这两个很重要;BEAM只是虚拟机,它没有进程、端口、ETS表等概念。它只执行指令,虽然ERTS影响了它们的设计,但它不影响代码运行时它们所做的事情,所以您不需要了解ERTS就可以理解BEAM。

BEAM是一个寄存器机器,所有指令都在指定的寄存器上操作,每个寄存器可以包含任何Erlang项,如整数或元组,因此可以将它们视为简单的变量。最重要的两种寄存器是:

X:它们用于临时数据和函数之间的数据传递,它们不需要堆栈帧,可以在任何函数中自由使用,但是有一些限制,我们稍后会详细说明。

Y:它们对于每个堆栈帧都是本地的,除了需要堆栈帧之外没有任何特殊的限制。

控制流由指令处理,这些指令测试某个条件,然后移到下一条指令或分支到其失败标签,用{f,Index}表示。例如{test,is_INTEGER,{f,7},[{x,0}]}。检查{x,0}是否包含整数,如果不包含,则跳至标签7。

函数参数在X寄存器中从左向右传递,从{x,0}开始,结果在{x,0}中返回。

通过示例可以更容易地解释它们是如何组合在一起的,所以让我们来演练一下以下几个部分:

SUM_Tail(LIST)->;SUM_Tail(LIST,0)。SUM_Tail([Head|Tail],ACC)->;SUM_Tail(Tail,Head+ACC);SUM_Tail([],ACC)->;ACC。

%%SUM_Tail/1,条目标签为2{Function,SUM_Tail,1,2}。%%用标签1.{label,1}标记跳转目标。%%引发Function_子句%%异常的特殊指令。在此函数中未使用。{func_info,{ATOM,Primer},{ATOM,SUM_Tail},1}。{标签,2}。%%该函数的主要部分从此处开始。%我们唯一的参数-列表-在{x,0}和%%中,因为SUM_Tail/2期望它是我们可以保留的第一个%%参数。我们将传递%%整数0作为{x,1}中的第二个参数。{移动,{整数,0},{x,1}}。%%Tail调用SUM_Tail/2,其条目标签为4。{CALL_ONLY,2,{f,4}}。%%SUM_Tail/2,条目标签为4{Function,SUM_Tail,2,4}。{标签,3}。{func_info,{ATOM,Primer},{ATOM,SUM_Tail},2}。{标签,4}。%%测试我们是否有非空列表,如果没有,则跳到标签5处的基本大小写%%。{test,is_non Empty_list,{f,5},[{x,0}]}。%%解压第一个参数中的列表,将%%头放入{x,2},将尾放入{x,0}。{get_list,{x,0},{x,2},{x,0}}。%%将头部和累加器相加(请记住,%%第二个函数参数位于{x,1}中),并将结果%%放入{x,1}中。%失败标签0表示我们希望%%指令在出错时抛出异常,而不是跳转到给定标签。{gc_bif,{#39;+';,{f,0},3,[{x,2},{x,1}],{x,1}}。%%Tail-调用我们自己来处理列表的其余部分,%%参数已经在正确的寄存器中。{CALL_ONLY,2,{f,4}}。{标签,5}。%%测试我们的参数是否为空列表。如果%%不是,我们跳到标签3以引发Function_子句%%异常。{test,is_nil,{f,3},[{x,0}]}。%%返回我们的累加器。{移动,{x,1},{x,0}}。回去吧。

不过,我忽略了一个小细节:附加说明中的神秘数字3。这个数字告诉我们有多少X寄存器保存实时数据,以防我们需要更多内存,这样它们就可以被保留,而其余的则作为垃圾丢弃。因此,在此指令之后引用更高的X寄存器是不安全的,因为它们的内容可能无效(在本例中为{x,3}或更高)。

函数调用与此类似;我们可以在每次调用函数或从函数返回时调度自己,并且在这样做时,我们将只保留函数参数/返回值。这意味着除了{x,0}之外的所有X寄存器在调用后都是无效的,即使您确定被调用的函数没有触及某个寄存器。

这就是Y寄存器进入画面的地方。让我们以前面的示例为例,将其改为正文递归:

{函数,SUM_BODY,1,7}。{标签,6}。{func_info,{ATOM,Primer},{ATOM,SUM_BODY},1}。{标签,7}。{test,is_non null_list,{f,8},[{x,0}]}。%%使用单个Y寄存器分配堆栈帧。%%由于此指令可能需要更多内存,我们%%告诉垃圾回收器,我们当前有%%个活动X寄存器({x,0}中的列表参数)。{分配,1,1}。%%解压列表,将头部放在{y,0}中,将尾部放在{x,0}中。{get_list,{x,0},{y,0},{x,0}}。%%身体-自称。请注意,虽然这会杀死所有%%X寄存器,但不会影响Y寄存器,因此我们的%%head仍然有效。{呼叫,1,{f,7}}。%%将头添加到我们的返回值中,并将%%结果存储在{x,0}中。{gc_bif,{#39;+';,{f,0},1,[{y,0},{x,0}],{x,0}}。%%释放堆栈帧并返回。{解除分配,1}。回去吧。{标签,8}。{test,is_nil,{f,6},[{x,0}]}。%%返回整数0。{移动,{整数,0},{x,0}}。回去吧。

现在我们在堆栈框架中,注意到调用指令是如何更改的吗?有三种不同的呼叫说明:

呼叫:示例中的普通呼叫。当被调用的函数返回时,控制流将在下一条指令处恢复。

CALL_LAST:有堆栈帧时的尾部调用。当前帧将在调用之前被释放。

它们中的每一个都有一个变量,用于调用其他模块中的函数(例如CALL_EXT),但是它们在其他方面是相同的。

到目前为止,我们只了解了如何使用术语,但是如何创建它们呢?我们来看看:

{函数,create_tuple,1,10}。{标签,9}。{func_info,{atom,primer},{atom,create_tuple},1}。{标签,10}。%%分配2元组所需的三个单词,%%使用活跃度注释1表示{x,0}%%是活动的,以防我们需要GC。{test_heap,3,1}。%%创建元组并将结果放在{x,0}{put_tuple2,{x,0},{list,[{atom,hello},{x,0}]}}中。回去吧。

这有点神奇,因为有一个看不见的内存分配寄存器,但分配很少远离使用,而且通常很容易跟踪。同样的原则也适用于PR2765之后的上市(合并)、浮动和基金。

更复杂的类型(如映射、大整数、引用等)是由特殊指令创建的,这些指令可能会自行GC(或在“堆片段”中分配到堆外部),因为它们的大小不能预先静态确定。

{函数,异常,0,12}。{标签,11}。{FUNC_INFO,{ATOM,PRIMER},{ATOM,EXCEPTION},0}。{标签,12}。{分配,1,0}。%%在{y,0}中放置Catch标记。如果在此标记是最新的标记%%时引发异常%%,则控制流将在此%%堆栈帧中的{f,13}处恢复。{';try';,{y,0},{f,13}}。{CALL_EXT,0,{extfunc,EXTERNAL,CALL,0}}。%%在返回调用结果%%之前停用CATCH标记。{try_end,{y,0}}。{解除分配,1}。回去吧。{标签,13}。%%哦,我们有个例外。取消Catch标记%%,并将异常类放在{x,0}中,将错误%%Reason/抛出值放在{x,1}中,将堆栈跟踪%%放在{x,2}中。{try_case,{y,0}}。如果用户抛出';示例';{test,is_eq_exact,{f,14},[{x,0},{ATOM,Throw}]},%%return';hello';。{test,is_eq_exact,{f,14},[{x,1},{ATOM,Example}]}。{Move,{atom,hello},{x,0}}。{解除分配,1}。回去吧。{标签,14}。%%否则,请重新引发异常,因为没有匹配的CATCH%%子句。{bif,RAISE,{f,0},[{x,2},{x,1}],{x,0}}。

到目前为止,您可能已经注意到控制流只向前移动;就像Erlang本身一样,循环的唯一方式是通过递归。唯一的例外是接收结构,它可能会循环,直到接收到匹配的消息:

{函数,SELECTIONAL_RECEIVE,1,16}。{标签,15}。{func_info,{ATOM,引物},{ATOM,SELECTIONAL_RECEIVE},1}。{标签,16}。{分配,1,1}。%%我们可能在等待%%消息时被安排出局,因此我们将在{y,0}中保留我们的引用。{移动,{x,0},{y,0}}。{标签,17}。%%从进程消息框%%中选取下一条消息,并将其放入{x,0}中,如果%%消息框为空,则跳至标签19。{loop_rec,{f,19},{x,0}}。%%它是否符合我们的模式?如果没有,请跳到标签18%%,然后尝试下一条消息。{test,is_tuple,{f,18},[{x,0}]}。{test,test_ality,{f,18},[{x,0},2]}。{get_tuple_element,{x,0},0,{x,1}}。{test,is_eq_exact,{f,18},[{x,1},{y,0}]}。%%我们已找到匹配项,请提取结果并从邮箱中删除%%邮件。{get_tuple_element,{x,0},1,{x,0}}。删除消息(_M)。{解除分配,1}。回去吧。{标签,18}。%%该消息与';不匹配,请循环回以处理我们的%%下一条消息。请注意,当前邮件仍保留在收件箱中的%%,因为其他接收者可能对其感兴趣。{LOOP_REC_END,{f,17}}。{LABEL,19}。%%等待,直到下一条消息到达,当下一条消息到达时,返回到循环开始时的%%。如果涉及%%的超时,将在此处处理。{等等,{f,17}}。

没有更多的内容,如果您愿意遵循上面的示例,那么使用JIT系列应该不会有任何问题。

如果您想知道有哪些指令,可以在genop.tab中找到每个指令的简要描述。