Erlang, Last Call Optimization, лямбда-функции и как предотвратить рост стека
Я пишу некоторый код 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
Из того, что я понимаю из этой статьи, Эрланг использует Оптимизацию по последнему вызову, где, если последнее, что делает функция, это вызывает другую функцию, BeamVM вместо этого переместит программный счетчик на начало новой функции вместо того, чтобы выдвигать новый стек Рамка. Означает ли это, что в схеме, подобной приведенной выше, мы топаем вокруг кучи, а не вокруг стека? Освобождает ли это память, которая ранее содержалась в этих функциях (в случае вышеприведенного кода, мы будем иметь одну копию функции F, выделенную в памяти за раз, или мы будем иметь много копий функции F, выделенной в памяти в время)? Есть ли какие-либо негативные последствия для использования этого шаблона (кроме очевидной борьбы с отладкой)?
2 ответа
Первый, fun
(лямбда, замыкание или как вы хотите это называть) из-за неизменной природы Erlang может быть реализован так, как вы можете представить себе как кортеж
{fun, {Module, FuncRef, Arity, CodeVersion}, CapturedValues}
Так что в вашем случае это будет что-то вроде
{fun, {recursive_test, '-a/2-fun-0-', 2, 2248400...}, [false, [2,3,4|...]]}
Обратите внимание, арность 2, потому что у вас арность 0 из fun
плюс 2 захваченных значения.
Тогда должно быть легче понять, что происходит в вашем коде. Помните, что это не настоящий кортеж, а некоторая структура, которая ведет себя очень схожим образом в отношении потребления данных, ссылок на кучи, GC, передачи по протоколу распределения Erlang и так далее.
Что позволяет вам понять второе, что это то, что призвание F()
внутри b/2
это что-то вроде
recursive_test:'-a/2-fun-0-'(false, [2,3,4|...])
который может быть хорошим хвостом, называемым aka jump. Итак, ваш код делает fun
"кортежи", а затем просто прыгать вокруг кода. каждый fun
На "кортеж" больше не ссылаются, поэтому его можно исключить в любое время. Я рекомендую вам попробовать это с номерами вместо списков и попробовать все больше и больше чисел и следить за потреблением памяти, используя process_info или наблюдателя. Это было бы хорошее упражнение.
Кстати, вы можете использовать
process_info(self(), stack_size)
вместо медленного и безобразного
roplists:get_value(stack_size, process_info(self()))
Это не ответ, но вы должны найти то, что ищете. Я скомпилировал версию вашего кода (без строки вызова io:format 7. Затем вы можете декомпилировать файл луча, чтобы увидеть, как интерпретируется код:
-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>