Haskell是否需要垃圾收集器



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

我想不出在纯语言中需要 GC 的情况。它只是为了减少复制而进行的优化,还是实际上有必要?

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

正如其他人已经指出的那样,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]必须保存在内存中,直到用户键入"清除";因此必须动态确定其生命周期,这就是动态内存管理需要的原因。

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

然而。。。

尽管垃圾回收

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

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

我们可能希望编译器检测到x2可以在f返回时安全地解除分配(而不是等待垃圾收集器x2释放(。本质上,我们要求编译器执行转义分析,以尽可能将垃圾回收堆中的分配转换为堆栈上的分配。

这要求并不是太不合理:JHC Haskell编译器这样做,尽管GHC没有。 Simon Marlow说,GHC的代际垃圾收集器使得逃逸分析几乎没有必要。

JHC实际上使用了一种复杂的逃逸分析形式,称为区域推理。考虑

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的区域。

超越哈斯克尔

虽然区域推断在上面讨论的某些情况下很有帮助,但似乎很难与惰性评估有效协调(参见 Edward Kmett 和 Simon Peyton Jones 的评论(。例如,考虑

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

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

在 1990 年代和 2000 年代初,在严格函数式语言 ML 的区域推断方面做了大量工作。 Mads Tofte、Lars Birkedal、Martin Elsman、Niels Hallenberg 写了一篇关于他们在区域推理方面的工作的相当可读的回顾,其中大部分内容都集成到了 MLKit 编译器中。他们尝试了纯粹基于区域的内存管理(即没有垃圾收集器(以及基于区域的混合/垃圾收集内存管理,并报告说他们的测试程序比纯垃圾收集版本"快10倍,慢4倍"。

让我们举一个简单的例子。 鉴于此

f (x, y)

在调用 f (x, y) 之前,您需要将对分配给某个地方。 什么时候可以解除该货币对的分配? 你不知道。 当f返回时,它不能被释放,因为f可能已经将货币对放在数据结构中(例如,f p = [p](,所以该对的生存期可能必须比从f返回的时间长。 现在,假设该货币对被放入一个列表中,谁将列表拆开,就可以解除该货币对的分配吗? 否,因为该对可能是共享的(例如,let p = (x, y) in (f p, p) (。 因此,很难判断该货币对何时可以解除分配。

哈斯克尔的几乎所有分配也是如此。 也就是说,有可能有一个分析(区域分析(来给出生命周期的上限。 这在严格语言中相当有效,但在惰性语言中则不那么有效(在实现中,惰性语言往往比严格语言做更多的突变(。

所以我想把问题转过来。 为什么你认为Haskell不需要GC。 您建议如何进行内存分配?

你认为这与纯洁有关,这有一定的道理。

Haskell被认为是纯粹的,部分原因是函数的副作用在类型签名中得到了考虑。因此,如果一个函数有打印某些东西的副作用,那么它的返回类型中一定有一个IO

但是有一个函数在Haskell中无处不在,其类型签名没有考虑到,从某种意义上说,这是一种副作用。即复制一些数据并返回两个版本的函数。在幕后,这可以通过复制内存中的数据从字面上工作,也可以通过增加以后需要偿还的债务来"虚拟"工作。

可以使用

更严格的类型系统(纯粹的"线性"系统(设计不允许复制函数的语言。从这种语言的程序员的角度来看,Haskell看起来有点不纯。

事实上,Clean是Haskell的亲戚,具有线性(更严格地说:唯一(类型,这可以让我们了解不允许复制会是什么样子。但 Clean 仍然允许复制"非唯一"类型。

这个领域有很多研究,如果你谷歌足够多,你会发现不需要垃圾收集的纯线性代码的例子。您会发现各种类型系统,它们可以向编译器发出信号,允许编译器消除一些 GC。

某种意义上说,量子算法也是纯线性的。每个操作都是可逆的,因此无法创建、复制或销毁任何数据。(它们在通常的数学意义上也是线性的。

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

另一种(更抽象的(思考方式是注意Haskell是由基于笛卡尔闭范畴理论的简单类型lambda演算建立的,并且这些范畴配备了对角函数diag :: X -> (X, X)。基于另一类类别的语言可能没有这样的东西。

但总的来说,纯线性规划太难了,没有用,所以我们选择了GC。

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

这就是为什么 GHC 程序往往具有如此高的总分配数字(从千兆字节到太字节(:它们不断分配内存,并且只有在耗尽之前才能回收它,这要归功于高效的 GC。

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

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

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

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

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

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

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

大概你的意思是一种纯粹的函数式语言。

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

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

  • 可能存在循环(例如,由let rec导致(这一事实意味着引用计数方法不适用于堆对象。

  • 然后是函数闭包...也不能在堆栈上分配,因为它们的生存期(通常(独立于创建它们的堆栈帧。

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

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

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

GC

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

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

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

Haskell是一种非严格编程语言,但大多数实现使用按需调用(懒惰(来实现非严格性。在按需调用中,您仅在运行时使用"thunks"机制(等待评估然后覆盖自己的表达式,保持可见以便在需要时重用其值(时对其进行评估。

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

最新更新