是否可以使用在Erlang中未优化尾调用的函数获取stackoverflow?例如,假设我有这样的函数
sum_list([],Acc) ->
Acc;
sum_list([Head|Tail],Acc) ->
Head + sum_list(Tail, Acc).
似乎如果在其中传递足够大的列表,最终会耗尽堆栈空间并崩溃。我尝试过这样测试:
> L = lists:seq(1, 10000000).
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22, 23,24,25,26,27,28,29|...]
> sum_test:sum_list(L, 0).
50000005000000
但它永远不会崩溃!我尝试了一个100,000,000整数的列表,它花了一段时间才完成,但它仍然没有崩溃!问题:
答案 0 :(得分:10)
您正在正确测试:您的函数确实不是尾递归的。要找到答案,您可以使用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
/ { {1}}非递归函数中的序列。
你没有得到堆栈溢出,因为Erlang“堆栈”非常大。实际上,堆栈溢出通常意味着处理器堆栈溢出,因为处理器的堆栈指针太远了。传统上,进程具有有限的堆栈大小,可以通过与操作系统交互来调整。例如,参见POSIX的setrlimit。
然而,Erlang执行堆栈不处理器堆栈,因为代码被解释。每个进程都有自己的堆栈,可以根据需要通过调用操作系统内存分配函数(在Unix上通常为malloc)来增长。
因此,只要return
调用成功,您的函数就不会崩溃。
对于记录,实际列表malloc
使用与堆栈相同的内存量来处理它。实际上,列表中的每个元素都有两个单词(整数值本身,由于它们很小而被加为单词)和指向列表中下一个元素的指针。相反,堆栈在每次迭代时由L
操作码生成两个单词:allocate
的一个单词由CP
本身保存,一个单词按要求保存({{1}的第一个参数1}})表示当前值。
对于64位VM上的100,000,000个单词,该列表至少需要1.5 GB(幸运的是,实际堆栈不会每两个字增长一次)。在shell中监视和编写这一点很困难,因为许多值仍然存在。如果生成函数,则可以看到内存使用情况:
allocate
如您所见,递归调用的内存不会立即释放。