为什么 s ++大s不会导致堆栈溢出吗?
我想知道为什么
Prelude> head $ reverse $ [1..10000000] ++ [99]
99
不会导致堆栈溢出错误。前奏中的 ++ 看起来很简单并且非尾递归:
(++) :: [a] -> [a] -> [a]
(++) [] ys = ys
(++) (x:xs) ys = x : xs ++ ys
编辑:最初,我认为这个问题与前奏中定义 ++ 的方式有关,特别是与重写规则有关,因此问题继续为以下。讨论告诉我事实并非如此。我现在认为一些惰性求值效应会导致代码在没有堆栈溢出的情况下运行,但我不太清楚如何实现。
所以仅仅这样,它应该会遇到堆栈溢出,对吗?所以我认为这可能与遵循 ++ 定义的 ghc 魔法有关:
{-# RULES “++”[~1] 代表所有 xs ys。 xs ++ ys = 增强 (\cn ->foldr cn xs) ys #-}
*这有助于避免堆栈溢出吗?有人可以提供一些关于这段代码中发生的事情的提示吗?**
I'm wondering why
Prelude> head $ reverse $ [1..10000000] ++ [99]
99
does not lead to a stack overflow error. The ++ in the prelude seems straight forward and non-tail-recursive:
(++) :: [a] -> [a] -> [a]
(++) [] ys = ys
(++) (x:xs) ys = x : xs ++ ys
EDIT: Initially, I thought the issue has something to do with the way ++ is defined in the prelude, especially with the rewriting rules, hence the question continued as below. The discussion showed me that this is not the case. I think now that some lazy evaluation effect causes the code to run without a stack overflow, but I don't quite figure how.
So just with this, it should run into a stack overflow, right? So I figure it probably has something to do with the ghc magic that follows the definition of ++:
{-# RULES
"++" [~1] forall xs ys. xs ++ ys = augment (\c n -> foldr c n xs) ys
#-}
*Is that what helps avoiding the stack overflow? Could someone provide some hint for what's going on in this piece of code?**
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
在 Haskell 中,非尾递归通常比尾递归更好,因为非尾递归的东西可能会很懒。您列出的定义比尾递归定义要好得多,因为尾递归定义会继续递归并生成整个列表,即使您只需要第一个元素;而非尾递归则只会做必要的工作。
Not-tail-recursive is often better than tail-recursive in Haskell, because not-tail-recursive things can be lazy. The definition you list there is much better than a tail-recursive one, because a tail-recursive one would keep recursing and generate the entire list, even if you need only the first element; whereas a non-tail recursive one would do only as much work as necessary.
这不会堆栈溢出 - 即使在没有优化和重写规则的解释器中 - 因为它不使用堆栈。
看一下 (++) 的定义,例如:
关键是 x : (xs ++ ys) ,也就是说,它是由 (:) “cons” 守护的递归构造函数。因为 Haskell 是惰性的,所以它为 cons 操作分配一个 thunk,并且递归调用会进入这个(堆分配的)thunk。所以你的堆栈分配现在是堆分配,它可以扩展很多。所以这一切所做的就是遍历大列表,在堆上分配新的 cons 对象来替换它正在遍历的对象。简单的!
“reverse”有点不同:
这是一个尾递归、累加器样式的函数,因此它会在堆上分配。
所以你看,这些函数依赖于在堆上使用 cons 单元,而不是在堆栈上,因此没有堆栈溢出。
为了真正解决这个问题,请查看 GC 虚拟机的运行时统计信息:
这是一个大列表 - 它是在堆上分配的,我们花费 80% 的时间来清理由 (++) 创建的 cons 节点。
经验教训:您通常可以用堆栈换取堆。
This doesn't stack overflow - even in the interpreter, where there are no optimizations and no rewrite rules - because it doesn't use the stack.
Look at the definition of (++), for example,:
The key thing is
x : (xs ++ ys)
-- that is, it is recursion guarded by the (:) "cons" constructor. Because Haskell is lazy, it allocates a thunk for the cons operation, and the recursive call goes onto this (heap-allocated) thunk. So your stack allocation is now heap allocation, which can expand quite a bit. So all this does is walk the big list, allocating new cons objects on the heap to replace the ones it is traversing. Easy!"reverse" is a bit different:
That is a tail recursive, accumulator-style function, so again, it will allocate on the heap.
So you see, the functions rely on using cons cells on the heap, instead of on the stack, hence no stack overflow.
To really nail this, look at the runtime stats from the GC vm:
There's your big list -- it is allocated on the heap, and we spend 80% of the time cleaning up cons nodes that are created by (++).
Lesson: you can often trade stack for heap.
编辑:下面的答案即使不是完全错误,也是完全无关的。 Don Stewart 证明了他实际上理解惰性求值,他有正确的解释。
如果运行 ghc -ddump-simpl ,您将看到正在使用的函数是 GHC.Base.++ 和 GHC.List.reverse 。代码>.这些函数经过精心设计,不会在大型列表上溢出堆栈。您在 Prelude 中看到的是“参考实现”,而不是实际编译的代码。
以下是我从 GHC 源代码分发中挖掘出的一些代码:
以及:
在第一种情况下,应该清楚发生了什么。在第二个中,我对重写规则有点模糊......
EDIT: The answer below is completely irrelevant, if not downright wrong. Don Stewart, who demonstrates that he actually understands lazy evaluation, has the right explanation.
If you run
ghc -ddump-simpl
, you'll see that the functions being used areGHC.Base.++
andGHC.List.reverse
. These functions are engineered not to overflow the stack on large lists. What you see in the Prelude is a "reference implementation," not the code that is actually compiled.Here's some code I dug out of the GHC source distribution:
And this:
In the first case, it should be clear what's going on. In the second, I'm a little fuzzy on the rewrite rules...