BEAM字节码指令call_last

问题描述

我们最近正在阅读BEAM Book作为阅读小组的一部分。

附录B.3.3中指出call_last指令具有以下行为

释放Deallocate个堆栈字,然后进行尾递归 在标签的同一模块中调用Arity函数Arity 标签

根据我们目前的理解,尾部递归意味着可以从当前调用中重用堆栈上分配的内存。

因此,我们想知道正在从堆栈中释放的内容。

此外,我们还想知道为什么需要在执行尾递归调用之前从堆栈中取消分配,而不是直接执行尾递归调用。

解决方法

在用于CPU的asm中,优化的tailcall只是跳转到函数入口点。即在尾递归的情况下,将整个函数作为循环体运行。 (无需推送返回地址,因此,当您达到基本情况时,仅是返回最终父项的返回。)

即使我对此一无所知,我也会大胆猜测Erlang / BEAM字节码非常相似。

当执行到达函数的顶部时,它不知道它是通过递归还是通过另一个函数的调用到达那里,因此如果需要,将不得不分配更多的空间。

如果要重用已经分配的堆栈空间,则必须进一步将尾部递归优化为函数体内的实际循环,而不再是递归。

或者换句话说,尾部调用任何东西,您都需要调用堆栈处于与函数入口处相同的状态。跳转而不是调用失去了被调用函数返回后 进行清理的机会,因为它返回给您的调用者,而不是返回给您。

但是我们不能只是将堆栈清理放到实际上返回而不是尾调用的递归基本情况中吗?是的,但是仅在“ tailcall”已分配到该函数中的某个点之后才起作用,而不是外部调用者将调用的入口点。这两个更改与将尾递归变成循环完全相同。

,

(免责声明:这是一个猜测)

尾部递归调用并不意味着它不能先前执行任何其他调用或在此期间使用堆栈。在这种情况下,必须在执行尾递归之前释放为这些调用分配的堆栈。 call_last在行为像call_only之前先释放剩余堆栈。

如果您erlc -S使用以下代码,您可以看到一个示例:

-module(test).
-compile(export_all).

fun1([]) ->
    ok;
fun1([1|R]) ->
    fun1(R).


funN() ->
    A = list(),B = list(),fun1([A,B]).

list() ->
    [1,2,3,4].

我已经注释了相关部分:

{function,fun1,1,2}.
  {label,1}.
    {line,[{location,"test.erl",4}]}.
    {func_info,{atom,test},fun1},1}.
  {label,2}.
    {test,is_nonempty_list,{f,3},[{x,0}]}.
    {get_list,{x,0},1},2}}.
    {test,is_eq_exact,{integer,1}]}.
    {move,2},0}}.
    {call_only,2}}. % No stack allocated,no need to deallocate it
  {label,3}.
    {test,is_nil,0}]}.
    {move,ok},0}}.
    return.


{function,funN,5}.
  {label,4}.
    {line,10}]}.
    {func_info,funN},0}.
  {label,5}.
    {allocate_zero,0}. % Allocate 1 slot in the stack
    {call,7}}. % Leaves the result in {x,0} (the 0 register)
    {move,{y,0}}.% Moves the previous result from {x,0} to the stack because next function needs {x,0} free
    {call,0} (the 0 register)
    {test_heap,4,1}.
    {put_list,nil,0}}. % Create a list with only the last value,[B]
    {put_list,0}}. % Prepend A (from the stack) to the previous list,creating [A,B] ([A | [B]]) in {x,0}
    {call_last,1}. % Tail recursion call deallocating the stack


{function,list,7}.
  {label,6}.
    {line,15}]}.
    {func_info,list},7}.
    {move,{literal,[1,4]},0}}.
    return.


编辑:
实际回答您的问题:
线程的内存用于堆栈和堆,它们在相反的方向使用相同的内存块,彼此相对增长(线程的GC在遇到时触发)。
在这种情况下,“分配”意味着增加用于堆栈的空间,如果不再要使用该空间,则必须将其释放(返回到内存块),以便以后可以再次使用它(两个作为堆或堆栈)。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...