您正确地测试了这一点:您的函数确实不是尾递归的。要找出答案,您可以使用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.
作为比较函数的以下尾递归版本:
tail_sum_list([],Acc) ->
Acc;
tail_sum_list([Head|Tail],Acc) ->
tail_sum_list(Tail, Head + Acc).
编译为:
{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.
注意尾递归版本中缺少allocate 和call_only 操作码,而不是非递归函数中的allocate/call/deallocate/return 序列。
您没有遇到堆栈溢出,因为 Erlang “堆栈”非常大。实际上,堆栈溢出通常意味着处理器堆栈溢出,因为处理器的堆栈指针离得太远了。进程传统上具有有限的堆栈大小,可以通过与操作系统交互来调整。参见例如 POSIX 的setrlimit。
但是,Erlang 执行堆栈不是处理器堆栈,因为代码是被解释的。每个进程都有自己的堆栈,可以通过调用操作系统内存分配函数(通常为 Unix 上的 malloc)按需增长。
因此,只要malloc 调用成功,您的函数就不会崩溃。
为了记录,实际列表L 使用与堆栈相同数量的内存来处理它。实际上,列表中的每个元素都包含两个单词(整数值本身,因为它们很小,所以被装箱为单词)和指向列表下一个元素的指针。相反,堆栈在每次迭代中由allocate 操作码增长两个字:一个字用于CP,由allocate 自己保存,一个字根据请求(allocate 的第一个参数)用于当前值.
对于 64 位 VM 上的 100,000,000 个字,该列表至少需要 1.5 GB(幸运的是,实际堆栈不是每两个字增长一次)。在 shell 中监控和垃圾处理是很困难的,因为许多值仍然存在。如果你生成一个函数,你可以看到内存使用情况:
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).
如你所见,递归调用的内存并没有立即释放。