簡體   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