Haskell需要垃圾收集器吗?

时间:2012-03-31 02:54:09

标签: haskell garbage-collection

我很好奇为什么Haskell实现使用GC。

我无法想到一个纯语言需要GC的情况。它只是减少复制的优化,还是实际需要?

我正在寻找在GC不存在时会泄漏的示例代码。

8 个答案:

答案 0 :(得分:213)

正如其他人已经指出的那样,Haskell需要自动动态内存管理:自动内存管理是必要的,因为手动内存管理是不安全的;动态内存管理是必要的,因为对于某些程序,对象的生命周期只能在运行时确定。

例如,请考虑以下程序:

main = loop (Just [1..1000]) where
  loop :: Maybe [Int] -> IO ()
  loop obj = do
    print obj
    resp <- getLine
    if resp == "clear"
     then loop Nothing
     else loop obj

在此程序中,列表[1..1000]必须保留在内存中,直到用户键入“clear”;所以的生命周期必须动态确定,这就是动态内存管理的必要原因。

所以从这个意义上说,自动动态内存分配是必要的,实际上这意味着:,Haskell需要一个垃圾收集器,因为垃圾收集是性能最高的自动动态内存管理器。 / p>

然而...

虽然垃圾收集器是必需的,但我们可能会尝试找到一些特殊情况,其中编译器可以使用比垃圾收集更便宜的内存管理方案。例如,给定

f :: Integer -> Integer
f x = let x2 = x*x in x2*x2

我们可能希望编译器检测到x2返回时可以安全地释放f(而不是等待垃圾收集器解除分配x2)。基本上,我们要求编译器执行escape analysis以尽可能将分配转换为垃圾回收堆到allocations on the stack

这要求不是太不合理:jhc haskell compiler这样做,尽管GHC没有。 Simon Marlow says GHC的世代垃圾收集器进行逃逸分析大多是不必要的。

jhc实际上使用了一种称为region inference的复杂形式的逃逸分析。考虑

f :: Integer -> (Integer, Integer)
f x = let x2 = x * x in (x2, x2+1)

g :: Integer -> Integer
g x = case f x of (y, z) -> y + z

在这种情况下,简单的转义分析会得出结论x2f转义(因为它在元组中返回),因此必须在垃圾上分配x2 - 收集堆。另一方面,区域推断能够检测到x2返回时g可以被解除分配;这里的想法是x2应该分配在g的区域而不是f的区域。

超越Haskell

虽然如上所述,区域推理在某些情况下有所帮助,但似乎很难与惰性评估进行有效协调(请参阅Edward Kmett'sSimon Peyton Jones'注释)。例如,考虑

f :: Integer -> Integer
f n = product [1..n]

有人可能会想要在堆栈上分配列表[1..n]并在f返回后解除分配,但这将是灾难性的:它将使用O(1)更改f内存(在垃圾收集下)到O(n)内存。

在20世纪90年代和21世纪初,人们对严格功能语言ML的区域推断进行了广泛的工作。 Mads Tofte,Lars Birkedal,Martin Elsman,Niels Hallenberg在他们关于区域推理的工作上写了一篇非常易读的retrospective,其中很多都融入了MLKit compiler。他们尝试了纯粹的基于区域的内存管理(即没有垃圾收集器)以及基于混合区域/垃圾收集的内存管理,并报告他们的测试程序比纯垃圾“运行速度快10倍,速度慢4倍” - 收集版本。

答案 1 :(得分:26)

让我们举一个小例子。鉴于此

f (x, y)

在调用(x, y)之前,您需要在某处分配f对。什么时候可以解除分配?你不知道。当f返回时,它不能被释放,因为f可能已将该对放在数据结构中(例如,f p = [p]),因此该对的生命周期可能必须长于返回来自f。现在,假设这对被列入一个列表中,那么将该列表分开的人可以取消分配该对吗?不,因为该对可能是共享的(例如let p = (x, y) in (f p, p))。因此,很难判断该对何时可以解除分配。

同样适用于Haskell中的几乎所有分配。也就是说,有可能进行分析(区域分析),给出生命周期的上限。这在严格的语言中工作得相当好,但在懒惰的语言中效果较差(在实现中,懒惰的语言往往比严格的语言做更多的突变)。

所以我想转过身来。为什么你认为Haskell不需要GC。您如何建议完成内存分配?

答案 2 :(得分:15)

你认为这与纯洁有关的直觉对它有一些道理。

Haskell被认为是纯粹的,部分原因是函数的副作用在类型签名中被考虑。因此,如果函数具有打印内容的副作用,则返回类型中必须存在IO

但是在Haskell中有一个隐式使用的函数,其类型签名没有考虑,在某种意义上,它是一个副作用。即复制某些数据并返回两个版本的函数。在引擎盖下,这可以通过在内存中复制数据来实现,也可以通过增加需要在以后偿还的债务来“虚拟”。

可以设计具有更严格的类型系统(纯粹的“线性”系统)的语言,这些系统不允许复制功能。从这种语言的程序员的角度来看,Haskell看起来有点不纯洁。

事实上,Clean是Haskell的一个亲戚,它具有线性(更严格地说:唯一)类型,并且可以让人知道禁止复制的内容。但Clean仍允许复制“非唯一”类型。

此区域有很多research,如果你足够Google,你会找到不需要垃圾回收的纯线性代码示例。您可以找到各种类型的系统,这些系统可以向编译器发出可能使用的内存信号,从而允许编译器消除某些GC。

有一种感觉,量子算法也是纯线性的。每个操作都是可逆的,因此不能创建数据copied或销毁。 (它们在通常的数学意义上也是线性的。)

与Forth(或其他基于堆栈的语言)进行比较也很有意思,它们具有明确的DUP操作,可以在发生重复时明确。

另一种(更抽象的)思考方式是注意Haskell是由简单的类型化的lambda演算构建的,它基于笛卡尔封闭类别的理论,并且这些类别具有对角函数{{1} }。基于另一类别的语言可能没有这样的东西。

但总的来说,纯线性编程太难用了,所以我们选择GC。

答案 3 :(得分:14)

应用于Haskell的标准实现技术实际上需要GC比大多数其他语言更多,因为它们从不改变先前的值,而是基于先前的值创建新的修改值。由于这意味着程序不断分配和使用更多内存,因此随着时间的推移,大量的值将被丢弃。

这就是GHC程序往往具有如此高的总分配数据(从千兆字节到太字节)的原因:它们不断分配内存,而且只有高效的GC才能在用完之前回收它。

答案 4 :(得分:10)

如果一种语言(任何语言)允许您动态分配对象,那么有三种实用的方法来处理内存管理:

  1. 该语言只允许您在堆栈上或启动时分配内存。但是这些限制严重限制了程序可以执行的计算类型。 (在实践中。理论上,您可以通过在一个大数组中表示它们来模拟(比如说)Fortran中的动态数据结构。它是可怕的......并且与此讨论无关。)

  2. 该语言可以提供明确的freedispose机制。但这依赖于程序员才能做到正确。存储管理中的任何错误都可能导致内存泄漏......或者更糟。

  3. 语言(或更严格地说,语言实现)可以为动态分配的存储提供自动存储管理器;即某种形式的垃圾收集器。

  4. 唯一的另一个选择是永远不会回收动态分配的存储。除了执行小型计算的小程序外,这不是一个实用的解决方案。

    将此应用于Haskell,该语言没有1.的限制,并且没有按照2的手动释放操作。因此,为了可用于非平凡的事情,Haskell实现需要包括垃圾收集器。

      

    我想不出用纯语言需要GC的情况。

    大概你的意思是一种纯粹的功能语言。

    答案是需要一个GC来回收语言必须创建的堆对象。例如。

    • 纯函数需要创建堆对象,因为在某些情况下它必须返回它们。这意味着它们无法在堆栈中分配。

    • 可能存在循环(例如由let rec产生)的事实意味着引用计数方法不适用于堆对象。

    • 然后有函数闭包...它们也不能在堆栈上分配,因为它们的生命周期(通常)与创建它们的堆栈帧无关。

      < / LI>
      

    我正在寻找在GC不存在时会泄漏的示例代码。

    几乎任何涉及闭包或图形数据结构的示例都会在这些条件下泄漏。

答案 5 :(得分:8)

如果您有足够的内存,则永远不需要垃圾收集器。但是,实际上,我们没有无限的内存,所以我们需要一些方法来回收不再需要的内存。在像C这样的不纯语言中,你可以明确地声明你已经完成了一些内存来释放它 - 但这是一个变异操作(你刚刚释放的内存不再是安全的读取),所以你不能使用这种方法一种纯粹的语言。所以它要么以某种方式静态分析你可以释放内存的位置(在一般情况下可能是不可能的),像筛子一样泄漏内存(在你用完之前效果很好),或者使用GC。

答案 6 :(得分:1)

Haskell是一种非严格的编程语言,但大多数实现都使用call-by-need(laziness)来实现非严格性。在按需调用时,您只能在运行时使用“thunks”机制(等待评估然后覆盖自身的表达式,在需要时保持可见值以便重新使用时)来评估内容。

所以,如果你使用thunks懒惰地实现你的语言,你已经推迟了所有关于对象生命周期的推理,直到最后一刻,即运行时。既然你现在对生命时间一无所知,你唯一能合理做的就是垃圾收集......

答案 7 :(得分:0)

GC在纯FP语言中是“必须拥有的”。为什么?运营分配和免费是不纯的!第二个原因是,不可变的递归数据结构需要GC存在,因为反向链接为人类思维创造了深奥和不可维护的结构。当然,反向链接是一种祝福,因为复制使用它的结构非常便宜。

无论如何,如果你不相信我,只要尝试实施FP语言,你会发现我是对的。

编辑:我忘记了。懒惰是没有GC的地狱。不相信我?只需在没有GC的情况下尝试它,例如C ++。你会看到......事情