【问题标题】:Tail-call recursive behavior of the BEAM bytecode instruction call_lastBEAM 字节码指令 call_last 的尾调用递归行为
【发布时间】:2020-08-23 12:22:25
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

    标签: recursion erlang bytecode tail-recursion beam


    【解决方案1】:

    在 CPU 的 asm 中,优化的尾调用只是跳转到函数入口点。 IE。在尾递归的情况下将整个函数作为循环体运行。 (没有推送返回地址,所以当你到达基本情况时,它只是返回到最终父级。)

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

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

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

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

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

    【讨论】:

      【解决方案2】:

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

      尾递归调用并不意味着它不能在之前执行任何其他调用或同时使用堆栈。在这种情况下,为这些调用分配的堆栈必须在执行尾递归之前释放。 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)。
      在这种情况下,“分配”意味着增加用于堆栈的空间,如果该空间不再使用,则必须将其释放(返回到内存块)以便以后能够再次使用它(要么作为堆或堆栈)。

      【讨论】:

        猜你喜欢
        • 2015-10-01
        • 2016-03-21
        • 1970-01-01
        • 2015-05-03
        • 1970-01-01
        • 1970-01-01
        • 2017-12-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多