You are testing this correctly : your function is really not tail recursive. To find out, you can compile your code with 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.
For comparison, the following tail-recursive version of the function:
tail_sum_list([],Acc) -> Acc; tail_sum_list([Head|Tail],Acc) -> tail_sum_list(Tail, Head + Acc).
compiles as:
{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.
Note the absence of the allocate
and call_only
in the tail recursive version, as opposed to the allocate
/ call
/ deallocate
/ return
sequence in the non-recursive function.
You do not get stack overflow because Erlang "stack" is very large. Indeed, a stack overflow usually means a processor stack overflow because the processor stack pointer has gone too far. Processes traditionally have a limited stack size that can be customized by interacting with the operating system. See, for example, POSIX setrlimit .
However, the Erlang execution stack is not a processor stack, as the code is interpreted. Each process has its own stack, which can grow as needed, calling the operating system memory allocation functions (usually malloc on Unix).
As a result, your function will not fail until malloc
calls success.
For writing, the actual list L
uses the same amount of memory as the stack to process it. In fact, each element in the list takes two words (the integer value itself, which is placed as a word as small) and a pointer to the next element in the list. In contrast, the stack is expressed in two words at each iteration on the allocate
: one word for CP
, which is saved by allocate
itself and one request (the first allocate
parameter) for the current value.
For 100,000,000 words on a 64-bit virtual machine, the list takes at least 1.5 GB (more since the actual stack does not grow every two words, fortunately). Monitoring and collecting garbage in the shell is difficult, as many values ββremain alive. If you create a function, you can see the memory usage:
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).
As you can see, the memory for the recursive call is not immediately freed.