【问题标题】:Why does s ++ t not lead to a stack overflow for large s?为什么s ++ t不会导致大s的堆栈溢出?
【发布时间】:2010-05-19 21:57:37
【问题描述】:

我想知道为什么

Prelude> head $ reverse $ [1..10000000] ++ [99]
99

不会导致堆栈溢出错误。前奏中的 ++ 似乎是直截了当且非尾递归的:

(++) :: [a] -> [a] -> [a]
(++) []     ys = ys
(++) (x:xs) ys = x : xs ++ ys

编辑:最初,我认为这个问题与前奏中定义 ++ 的方式有关,特别是与重写规则有关,因此问题继续如下。讨论告诉我,事实并非如此。我现在认为某些惰性求值效果会导致代码在没有堆栈溢出的情况下运行,但我不太清楚如何。

所以就这样,它应该会遇到堆栈溢出,对吗?所以我认为这可能与遵循 ++ 定义的 ghc 魔法有关:

{-# 规则 "++" [~1] 对于所有 xs ys。 xs ++ ys = 增加 (\c n -> foldr c n xs) ys #-}

*这有助于避免堆栈溢出吗?有人可以为这段代码中发生的事情提供一些提示吗?**

【问题讨论】:

  • 重写规则不会在解释器中触发(除非您启用它们)。
  • @Don:谢谢,我没有启用它们。无论如何,我应该在输入之前检查一下:A new function "f s t = if s == [] then t else let (x:ss) = s in x:(f ss t)" 也不会导致堆栈溢出,因此它与规则部分没有任何关系......

标签: haskell lazy-evaluation ghc tail-recursion


【解决方案1】:

前奏中的 ++ 似乎是直截了当且非尾递归的......所以就这样,它应该会遇到堆栈溢出,对吧?

在 Haskell 中,非尾递归通常比尾递归更好,因为非尾递归的东西可能是惰性的。您在那里列出的定义比尾递归的定义要好得多,因为尾递归的定义会继续递归并生成整个列表,即使您只需要第一个元素;而非尾递归只会做尽可能多的工作。

【讨论】:

  • ++ 的尾递归定义是否总是导致单一函数?有人可以为此概述一个证据吗?
【解决方案2】:

这不会堆栈溢出 - 即使在没有优化和重写规则的解释器中 - 因为它不使用堆栈。

看(++)的定义,例如:

(++) :: [a] -> [a] -> [a]
(++) []     ys = ys
(++) (x:xs) ys = x : xs ++ ys

关键是x : (xs ++ ys)——也就是说,它是由 (:) "cons" 构造函数保护的递归。因为 Haskell 是惰性的,它为 cons 操作分配了一个 thunk,递归调用转到这个(堆分配的)thunk。所以你的堆栈分配现在是堆分配,它可以扩展很多。所以这一切都是遍历大列表,在堆上分配新的 cons 对象来替换它正在遍历的那些。简单!

“反向”有点不同:

reverse l =  rev l []
  where
    rev []     a = a
    rev (x:xs) a = rev xs (x:a)

这是一个尾递归、累加器风格的函数,所以同样,它将在堆上分配。

如你所见,函数依赖于在堆上使用 cons 单元,而不是在堆栈上,因此没有堆栈溢出。

要真正确定这一点,请查看 GC vm 的运行时统计信息:

$ time ./B +RTS -s
99

         833 MB total memory in use (13 MB lost due to fragmentation)
         Generation 0:  3054 collections,     0 parallel,  0.99s,  1.00s elapsed
         %GC time      82.2%  (85.8% elapsed)

这是你的大列表——它被分配在堆上,我们花费 80% 的时间清理由 (++) 创建的 cons 节点。

教训:你经常可以用栈换堆。

【讨论】:

  • +1 因为是 SO 上唯一真正理解惰性评估的人。我早期的机器学习训练让我永远残废:-(
【解决方案3】:

编辑:下面的答案是完全不相关的,如果不是完全错误的话。唐·斯图尔特(Don Stewart)证明了他实际上理解惰性评估,他有正确的解释。


如果您运行ghc -ddump-simpl,您会看到正在使用的函数是GHC.Base.++GHC.List.reverse。这些函数被设计为不会溢出大型列表上的堆栈。您在 Prelude 中看到的是“参考实现”,而不是实际编译的代码。

这是我从 GHC 源代码分发中挖出的一些代码:

reverse                 :: [a] -> [a]
#ifdef USE_REPORT_PRELUDE
reverse                 =  foldl (flip (:)) []
#else
reverse l =  rev l []
  where
    rev []     a = a
    rev (x:xs) a = rev xs (x:a)
#endif

还有这个:

(++) :: [a] -> [a] -> [a]
(++) []     ys = ys
(++) (x:xs) ys = x : xs ++ ys

{-# RULES
"++"    [~1] forall xs ys. xs ++ ys = augment (\c n -> foldr c n xs)     ys
  #-}

在第一种情况下,应该清楚发生了什么。其次,我对重写规则有点模糊......

【讨论】:

  • 谢谢,很有趣!但是,当我查看 GHC.Base 时,我仍然看到普通的旧 ++ 定义,没有反转或发生任何事情。更奇怪的是:当我编写自己的附加函数(与 ++ 相同的定义,只是不同的名称)并编译它时,GHC 给了我相同的转储和混合中的反向函数...... GHC 很神奇...... :- )
  • GHC 说augment 有助于避免中间列表。所以它可以读作foldr (:) ys xs,但不是从ysxs 构建列表,而是将xs 就地转换为产生具有不同尾部的列表的函数。
  • 我认为这不是 GHC.Base 中 ++ 的定义,因为它也适用于我自己的附加函数。我认为这可能是一些我不太明白的惰性评估:(s:ss)++t 的每次迭代都会生成一个列表 s:(ss++t),然后这个 s 立即被反向“带走” .也许这种行为“减轻”了 (++) 的调用堆栈?但是怎么做呢?
猜你喜欢
  • 2014-12-20
  • 1970-01-01
  • 1970-01-01
  • 2018-07-01
  • 2010-09-11
  • 1970-01-01
  • 2011-01-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多