Recursion Erlang、最后一次调用优化、lambda函数以及如何防止堆栈增长
我正在写一些Erlang代码,遇到了一个我不理解的奇怪情况 守则: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)]
-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代码。感谢
过程信息
呼叫-我将编辑原始问题以将其包括在内,以便删除糟糕的节目列表
呼叫。再次感谢!