Recursion Erlang、最后一次调用优化、lambda函数以及如何防止堆栈增长

Recursion Erlang、最后一次调用优化、lambda函数以及如何防止堆栈增长,recursion,erlang,tail-call-optimization,tail-call,Recursion,Erlang,Tail Call Optimization,Tail Call,我正在写一些Erlang代码,遇到了一个我不理解的奇怪情况 守则: -module(recursive_test). -export([a/2]). a(_, []) -> ok; a(Args, [H|T]) -> F = fun() -> a(Args, T) end, io:fwrite( "~nH: ~p~nStack Layers: ~p", [H, process_info(self(), stack_size)]

我正在写一些Erlang代码,遇到了一个我不理解的奇怪情况

守则:

-module(recursive_test).
-export([a/2]).

a(_, []) -> ok;
a(Args, [H|T]) ->
    F = fun() -> a(Args, T) end,
    io:fwrite(
        "~nH: ~p~nStack Layers: ~p",
        [H, process_info(self(), stack_size)]
    ),
    b(Args, F).

b(Args, F) ->
    case Args of
        true -> ok;
        false -> F()
    end.
输出:

(Erlang/OTP 20)
12> c(recursive_test).
{ok,recursive_test}
13> recursive_test:a(false, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).
H: 1
Stack Layers: 28
H: 2
Stack Layers: 28
H: 3
Stack Layers: 28
H: 4
Stack Layers: 28
H: 5
Stack Layers: 28
H: 6
Stack Layers: 28
H: 7
Stack Layers: 28
H: 8
Stack Layers: 28
H: 9
Stack Layers: 28
H: 10
Stack Layers: 28
ok
14> recursive_test:a(false, [1, 2, 3, 4, 5, 6]).
H: 1
Stack Layers: 28
H: 2
Stack Layers: 28
H: 3
Stack Layers: 28
H: 4
Stack Layers: 28
H: 5
Stack Layers: 28
H: 6
Stack Layers: 28
ok

从我的理解来看,Erlang使用了最后一个调用优化,如果一个函数做的最后一件事是调用另一个函数,那么BeamVM将把程序计数器跳到新函数的开始处,而不是推一个新的堆栈帧。这是否意味着在上面这样的模式中,我们是在堆而不是堆栈周围跺脚?这是否释放了以前保存在这些函数中的内存(在上述代码的情况下,是一次在内存中分配一个函数F的副本,还是一次在内存中分配多个函数F的副本)?使用此模式是否会产生任何负面影响(除了明显的调试困难)?

这不是答案,但您应该找到您想要的。我已经编译了您的代码的一个版本(没有io:format调用第7行。然后您可以反编译beam文件,查看如何解释代码:

-module(recursive_test).
-export([a/2]).

a(_, []) -> ok;
a(Args, [H|T]) ->
    F = fun() ->
            a(Args, T)
        end,
    b(Args, F).

b(Args, F) ->
    case Args of
        true -> ok;
        false -> F()
    end.
在外壳中:

15> c(recursive_test).                      
recursive_test.erl:5: Warning: variable 'H' is unused
{ok,recursive_test}
16> rp(beam_disasm:file(recursive_test)).
{beam_file,recursive_test,
           [{a,2,2},{module_info,0,10},{module_info,1,12}],
           [{vsn,[224840029366305056373101858936888814401]}],
           [{version,"7.2.1"},
            {options,[]},
            {source,"c:/git/fourretout/src/recursive_test.erl"}],
           [{function,a,2,2,
                      [{label,1},
                       {line,1},
                       {func_info,{atom,recursive_test},{atom,a},2},
                       {label,2},
                       {test,is_nonempty_list,{f,3},[{x,1}]},
                       {allocate,1,2},
                       {get_tl,{x,1},{x,1}},
                       {move,{x,0},{y,0}},
                       {make_fun2,{recursive_test,'-a/2-fun-0-',2},0,88683754,2},
                       {move,{x,0},{x,1}},
                       {move,{y,0},{x,0}},
                       {call_last,2,{recursive_test,b,2},1},
                       {label,3},
                       {test,is_nil,{f,1},[{x,1}]},
                       {move,{atom,ok},{x,0}},
                       return]},
            {function,b,2,5,
                      [{line,2},
                       {label,4},
                       {func_info,{atom,recursive_test},{atom,b},2},
                       {label,5},
                       {test,is_atom,{f,8},[{x,0}]},
                       {select_val,{x,0},
                                   {f,8},
                                   {list,[{atom,true},{f,6},{atom,false},{f,7}]}},
                       {label,6},
                       {move,{atom,ok},{x,0}},
                       return,
                       {label,7},
                       {allocate,0,2},
                       {move,{x,1},{x,0}},
                       {line,3},
                       {call_fun,0},
                       {deallocate,0},
                       return,
                       {label,8},
                       {line,4},
                       {case_end,{x,0}}]},
            {function,module_info,0,10,
                      [{line,0},
                       {label,9},
                       {func_info,{atom,recursive_test},{atom,module_info},0},
                       {label,10},
                       {move,{atom,recursive_test},{x,0}},
                       {line,0},
                       {call_ext_only,1,{extfunc,erlang,get_module_info,1}}]},
            {function,module_info,1,12,
                      [{line,0},
                       {label,11},
                       {func_info,{atom,recursive_test},{atom,module_info},1},
                       {label,12},
                       {move,{x,0},{x,1}},
                       {move,{atom,recursive_test},{x,0}},
                       {line,0},
                       {call_ext_only,2,{extfunc,erlang,get_module_info,2}}]},
            {function,'-a/2-fun-0-',2,14,
                      [{line,5},
                       {label,13},
                       {func_info,{atom,recursive_test},{atom,'-a/2-fun-0-'},2},
                       {label,14},
                       {call_only,2,{recursive_test,a,2}}]}]}
ok
17>
首先,
fun
(lambda、closure或任何你想称之为它的东西)由于Erlang的不可变性质,可以并且可以用一种你可以想象的类似于tuple的方式实现

{fun, {Module, FuncRef, Arity, CodeVersion}, CapturedValues}
所以在你的情况下,它会是

{fun, {recursive_test, '-a/2-fun-0-', 2, 2248400...}, [false, [2,3,4|...]]}
recursive_test:'-a/2-fun-0-'(false, [2,3,4|...])
注意,arity是2,因为您有
fun
的arity 0加上2个捕获的值

记住,不是真正的元组,而是在数据消耗、堆项引用、GC、围绕Erlang分发协议的传输等方面表现非常相似的结构

这使您能够理解第二件事,即在
b/2
内部调用
F()
类似于

{fun, {recursive_test, '-a/2-fun-0-', 2, 2248400...}, [false, [2,3,4|...]]}
recursive_test:'-a/2-fun-0-'(false, [2,3,4|...])
这可能是一个很好的尾部调用,也称为跳跃。因此,您的代码使
有趣
“元组”,然后只是在代码周围跳跃。每个
有趣
“元组”然后就不再被引用了,所以可以随时被GCed出来。我建议你尝试使用数字而不是列表,尝试越来越大的数字,并使用进程信息或观察者观察内存消耗。这将是一个很好的练习

顺便说一句,你可以使用

process_info(self(), stack_size)
而不是缓慢和丑陋

roplists:get_value(stack_size, process_info(self()))

谢谢你的回答-看起来我需要学习更多关于Erlang的中间形式的知识,然后才能在这里深入研究。当我弄明白IF是什么意思时,我会编辑问题并提供答案。谢谢你提供的信息-我会尝试你建议的一些实验。我不确定我是否足够聪明,能够从逻辑上对此进行推理s代码。感谢
过程信息
呼叫-我将编辑原始问题以将其包括在内,以便删除糟糕的
节目列表
呼叫。再次感谢!