繁体   English   中英

BEAM 字节码指令 call_last 的尾调用递归行为

[英]Tail-call recursive behavior of the BEAM bytecode instruction call_last

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

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

Deallocate Deallocate分配栈的单词,然后对标签Label处同一个模块中arity Arity的函数做尾递归调用

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

因此,我们想知道从堆栈中释放了什么。

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

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

我会疯狂地猜测 Erlang / BEAM 字节码是非常相似的,尽管我对此一无所知。

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

如果你想重用已经分配的堆栈空间,你必须进一步优化尾递归到函数体内的实际循环中,而不是递归。

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

但是我们不能将堆栈清理放在实际返回而不是尾调用的递归基本情况中吗? 是的,但只有在分配新空间后“尾调用”指向此函数中的某个点时才有效,而不是外部调用者将调用的入口点。 这两个更改与将尾递归转换为循环完全相同。

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

尾递归调用并不意味着它之前不能执行任何其他调用或同时使用堆栈。 在这种情况下,必须在执行尾递归之前释放为这些调用分配的堆栈。 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},{atom,fun1},1}.
  {label,2}.
    {test,is_nonempty_list,{f,3},[{x,0}]}.
    {get_list,{x,0},{x,1},{x,2}}.
    {test,is_eq_exact,{f,1},[{x,1},{integer,1}]}.
    {move,{x,2},{x,0}}.
    {call_only,1,{f,2}}. % No stack allocated, no need to deallocate it
  {label,3}.
    {test,is_nil,{f,1},[{x,0}]}.
    {move,{atom,ok},{x,0}}.
    return.


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


{function, list, 0, 7}.
  {label,6}.
    {line,[{location,"test.erl",15}]}.
    {func_info,{atom,test},{atom,list},0}.
  {label,7}.
    {move,{literal,[1,2,3,4]},{x,0}}.
    return.


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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM