Erlang:stackoverflow与递归函数不是尾调用优化?

时间:2014-11-19 23:12:26

标签: recursion erlang stack-overflow tail-call-optimization

是否可以使用在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整数的列表,它花了一段时间才完成,但它仍然没有崩溃!问题:

  1. 我是否正确测试了这个?
  2. 如果是这样,为什么我无法生成stackoverflow?
  3. Erlang是否正在做一些阻止堆栈溢出发生的事情?

1 个答案:

答案 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.

请注意尾递归版本中缺少allocatecall_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

如您所见,递归调用的内存不会立即释放。