简体   繁体   English

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

[英]Erlang: stackoverflow with recursive function that is not tail call optimized?

Is it possible to get a stackoverflow with a function that is not tail call optimized in Erlang? 是否有可能在Erlang中获得一个不是尾部调用优化函数的stackoverflow? For example, suppose I have a function like this 例如,假设我有这样的功能

sum_list([],Acc) ->
   Acc;
sum_list([Head|Tail],Acc) ->
   Head + sum_list(Tail, Acc).

It would seem like if a large enough list was passed in it would eventually run out of stack space and crash. 看起来如果在它中传递足够大的列表最终会耗尽堆栈空间并崩溃。 I tried testing this like so: 我尝试过这样测试:

> 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

But it never crashes! 但它永远不会崩溃! I tried it with a list of 100,000,000 integers and it took a while to finish but it still never crashed! 我尝试了一个100,000,000整数的列表,它花了一段时间才完成,但它仍然没有崩溃! Questions: 问题:

  1. Am I testing this correctly? 我正确测试了吗?
  2. If so, why am I unable to generate a stackoverflow? 如果是这样,为什么我无法生成stackoverflow?
  3. Is Erlang doing something that prevents stackoverflows from occurring? Erlang是否正在做一些阻止堆栈溢出发生的事情?

You are testing this correctly : your function is indeed not tail-recursive. 您正在对此进行正确测试 :您的函数确实不是尾递归。 To find out, you can compile your code using erlc -S <erlang source file> . 为了找到erlc -S <erlang source file> ,您可以使用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.

As a 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.

Notice the lack of allocate and the call_only opcode in the tail-recursive version, as opposed to the allocate / call / deallocate / return sequence in the non-recursive function. 注意尾递归版本中缺少allocatecall_only操作码,而非递归函数中的allocate / call / deallocate / return序列。

You are not getting a stack overflow because the Erlang "stack" is very large. 你没有得到堆栈溢出,因为Erlang“堆栈”非常大。 Indeed, stack overflow usually means the processor stack overflowed, as the processor's stack pointer went too far away. 实际上,堆栈溢出通常意味着处理器堆栈溢出,因为处理器的堆栈指针太远了。 Processes traditionally have a limited stack size which can be tuned by interacting with the operating system. 传统上,进程具有有限的堆栈大小,可以通过与操作系统交互来调整。 See for example POSIX's setrlimit . 请参阅例如POSIX的setrlimit

However, Erlang execution stack is not the processor stack, as the code is interpreted. 但是,Erlang执行堆栈不是处理器堆栈,因为代码被解释。 Each process has its own stack which can grow as needed by invoking operating system memory allocation functions (typically malloc on Unix). 每个进程都有自己的堆栈,可以根据需要通过调用操作系统内存分配函数(通常是Unix上的malloc )来增长。

As a result, your function will not crash as long as malloc calls succeed. 因此,只要malloc调用成功,您的函数就不会崩溃。

For the record, the actual list L is using the same amount of memory as the stack to process it. 对于记录,实际列表L使用与堆栈相同的内存量来处理它。 Indeed, each element in the list takes two words (the integer value itself, which is boxed as a word as they are small) and the pointer to the next element to the list. 实际上,列表中的每个元素都有两个单词(整数值本身,由于它们很小而被加为单词)和指向列表中下一个元素的指针。 Conversely, the stack is grown by two words at each iteration by allocate opcode: one word for CP which is saved by allocate itself and one word as requested (the first parameter of allocate ) for the current value. 相反,堆栈在每次迭代时通过allocate操作码生成两个字: CP一个字,通过allocate自身和一个字作为当前值的请求( allocate的第一个参数)来保存。

For 100,000,000 words on a 64-bit VM, the list takes a minimum of 1.5 GB (more as the actual stack is not grown every two words, fortunately). 对于64位VM上的100,000,000个单词,该列表至少需要1.5 GB(幸运的是,实际堆栈不会每两个字增长一次)。 Monitoring and garbaging this is difficult in the shell, as many values remain live. 在shell中监视和编写这一点很困难,因为许多值仍然存在。 If you spawn 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 released immediately. 如您所见,递归调用的内存不会立即释放。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM