Testujesz to poprawnie: Twoja funkcja nie jest rekursywna. Aby się o tym przekonać, możesz skompilować swój kod za pomocą erlc -S <erlang source file>
.
{function, sum_list, 2, 2}.
{label,1}.
{func_info,{atom,so},{atom,sum_list},2}.
{label,2}.
{test,is_nonempty_list,{f,3},[{x,0}]}.
{allocate,1,2}.
{get_list,{x,0},{y,0},{x,0}}.
{call,2,{f,2}}.
{gc_bif,'+',{f,0},1,[{y,0},{x,0}],{x,0}}.
{deallocate,1}.
return.
{label,3}.
{test,is_nil,{f,1},[{x,0}]}.
{move,{x,1},{x,0}}.
return.
porównania następujące ogona rekurencyjne wersja funkcji:
tail_sum_list([],Acc) ->
Acc;
tail_sum_list([Head|Tail],Acc) ->
tail_sum_list(Tail, Head + Acc).
zestawia jako:
{function, tail_sum_list, 2, 5}.
{label,4}.
{func_info,{atom,so},{atom,tail_sum_list},2}.
{label,5}.
{test,is_nonempty_list,{f,6},[{x,0}]}.
{get_list,{x,0},{x,2},{x,3}}.
{gc_bif,'+',{f,0},4,[{x,2},{x,1}],{x,1}}.
{move,{x,3},{x,0}}.
{call_only,2,{f,5}}.
{label,6}.
{test,is_nil,{f,4},[{x,0}]}.
{move,{x,1},{x,0}}.
return.
zauważyć brak allocate
i call_only
opcode w tail- wersja rekursywna, w przeciwieństwie do wersji /sekwencja w funkcji nierekurencyjnej.
Nie otrzymujesz przepełnienia stosu, ponieważ "stos" Erlanga jest bardzo duży. Rzeczywiście przepełnienie stosu zwykle oznacza przepełnienie stosu procesora, ponieważ wskaźnik stosu procesora wyszedł zbyt daleko. Procesy tradycyjnie mają ograniczony rozmiar stosu, który można dostroić poprzez interakcję z systemem operacyjnym. Zobacz na przykład POSIX's setrlimit.
Jednak stos wywoływania Erlanga to , a nie stos procesora, ponieważ kod jest interpretowany. Każdy proces ma swój własny stos, który może rosnąć w miarę potrzeby poprzez wywoływanie funkcji alokacji pamięci systemu operacyjnego (zazwyczaj malloc na systemie Unix).
W wyniku tego funkcja nie ulegnie awarii, o ile połączenia malloc
zakończą się powodzeniem.
Dla celów rzeczywistej listy L
używa się tej samej ilości pamięci, co stos do jej przetworzenia. Rzeczywiście, każdy element na liście przyjmuje dwa słowa (samą wartość całkowitą, która jest zapakowana w słowo, ponieważ są małe) i wskaźnik do następnego elementu do listy. Odwrotnie, stos jest rozwijany przez dwa słowa w każdej iteracji kodem allocate
: jedno słowo dla CP
, które jest zapisywane przez allocate
i jedno słowo jako żądane (pierwszy parametr allocate
) dla bieżącej wartości.
Dla 100 000 000 słów na 64-bitowej maszynie wirtualnej lista zajmuje co najmniej 1,5 GB (więcej, niż faktyczny stos nie rośnie co dwa słowa, na szczęście). Monitorowanie i garbowanie tego jest trudne w powłoce, ponieważ wiele wartości pozostaje aktywnych. Jeśli tarło funkcję, można zobaczyć wykorzystanie pamięci:
spawn(fun() ->
io:format("~p\n", [erlang:memory()]),
L = lists:seq(1, 100000000),
io:format("~p\n", [erlang:memory()]),
sum_test:sum_list(L, 0),
io:format("~p\n", [erlang:memory()])
end).
Jak widać, pamięć dla wywołania rekurencyjnego nie jest uwalniany natychmiast.
Właśnie sprawdzałem to i myślę, że nie mam racji co do stałej przestrzeni: stos rośnie we wszystkich rekurencjach ciała, ale stos Erlanga jest znacznie bardziej wydajny niż do tej pory. Optymalizacja, o której myślałem, była drastycznym wzrostem * prędkości *, a nie przestrzeni, dzięki czemu rekurencja ciała była porównywalna do rekurencji ogona. Wypróbuj swój test z bilionami liczb całkowitych. – zxq9