Haskell是否需要垃圾回收器?

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

我不能想到一个纯语言中需要GC的情况。 这只是减less复制的优化,还是实际上是必要的?

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

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

例如,考虑以下程序:

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]必须保存在内存中,直到用户键入“清除”。 所以这个生命周期必须dynamic确定,这就是为什么dynamic内存pipe理是必要的。

所以在这个意义上说,自动dynamic内存分配是必要的,实际上这意味着: 是的 ,Haskell需要一个垃圾回收器,因为垃圾回收是性能最高的自动dynamic内存pipe理器。

然而…

虽然垃圾收集器是必要的,但是我们可能会尝试find一些特殊情况,编译器可以使用比垃圾收集更便宜的内存pipe理scheme。 例如,给定

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

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

这不是太不合理的要求: jhc haskell编译器这样做,虽然GHC不。 西蒙马洛说 ,GHC的世代垃圾收集器使逃生分析大多是不必要的。

jhc实际上使用了一种复杂的逃生分析forms,即区域推断 。 考虑

 f :: Integer -> (Integer, Integer) fx = let x2 = x * x in (x2, x2+1) g :: Integer -> Integer gx = case fx of (y, z) -> y + z 

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

超越Haskell

如上所述,区域推理在某些情况下是有帮助的,但看起来很难与惰性评估有效地协调一致(参见Edward Kmett和Simon Peyton Jones的评论)。 例如,考虑一下

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

有人可能会试图在栈上分配列表[1..n] ,并在f返回后释放它,但是这将是灾难性的:它将从使用O(1)内存(在垃圾收集下)到O(n )内存。

在20世纪90年代和21世纪初,为严格的function语言ML进行了广泛的工作。 Mads Tofte,Lars Birkedal,Martin Elsman和Niels Hallenberg对他们在区域推理方面的工作做了一个相当可读的回顾 ,其中大部分都集成到了MLKit编译器中 。 他们尝试了纯粹的基于区域的内存pipe理(即没有垃圾收集器)以及混合的基于区域的/垃圾收集的内存pipe理,并且报告说他们的testing程序的运行速度比纯垃圾回收“慢10倍到4倍”收集版本。

我们举一个简单的例子。 鉴于这种

 f (x, y) 

您需要在调用f之前在某处分配pair (x, y) 。 你什么时候可以取消配对? 你不知道。 当f返回时,它不能被解除分配,因为f可能会把这个对放在一个数据结构中(例如fp = [p] ),所以这个对的生命周期可能要比从f返回的时间长。 那么现在呢,说这两个人是放在一个单子里的,那么谁可以把这个单子分开呢? 不,因为这个对可能是共享的(例如, let p = (x, y) in (fp, p) )。 所以很难分辨何时可以解除分配。

Haskell中几乎所有的分配都是一样的。 也就是说,可能有一个分析(区域分析),给出了生命周期的上限。 这在严格的语言中工作得相当好,但在懒惰语言中则less得多(懒惰的语言比实现中的严格语言更倾向于进行更多的突变)。

所以我想回答这个问题。 为什么你认为Haskell不需要GC? 你会如何build议内存分配?

你的直觉认为这与纯洁有关,这是有道理的。

Haskell被认为是纯粹的,部分原因是函数的副作用在types签名中被考虑。 所以如果一个函数有打印某些东西的副作用,那么它的返回types必须有一个IO

但是有一个函数在Haskell中被隐式地使用,并且其types签名没有被解释,在某种意义上什么是副作用。 即复制一些数据的function,给你两个版本。 在这种情况下,这可以通过在内存中复制数据来实现,或者通过增加债务来“虚拟”,之后需要还款。

可以devise具有更严格的types系统(纯粹“线性”系统)的语言,该系统不允许复制function。 从这样一种语言的程序员的angular度来看,Haskell看起来有点不纯。

实际上,Haskell的一个亲戚Clean具有线性(更严格的说是唯一的)types,这可以让我们知道不允许拷贝是什么样的。 但是Clean仍然允许复制“非唯一”types。

在这方面有很多的研究 ,如果你足够Google,你会发现不需要垃圾收集的纯线性代码的例子。 你会发现各种types的系统,可以告诉编译器什么样的内存可以使用,使编译器消除一些GC。

量子algorithm也是纯线性的。 每个操作都是可逆的,所以不能创build, 复制或销毁数据。 (它们在通常的math意义上也是线性的。)

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

另一种(更抽象的)思考方式是注意Haskell是由简单types的lambda演算构build的,它基于笛卡尔封闭类的理论,并且这种类包含一个对angular函数diag :: X -> (X, X) 。 基于另一类别的语言可能没有这种东西。

但总的来说,纯粹的线性编程太难以用了,所以我们解决了GC问题。

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

这就是为什么GHC程序的总分配数字(从千兆字节到兆兆字节)往往是这样高的:它们不断地分配内存,这只是感谢高效的GC在用完之前将其回收。

如果一种语言(任何语言)允许你dynamic地分配对象,那么有三种实际的方式来处理内存的pipe理:

  1. 该语言只能允许您在堆栈上或启动时分配内存。 但是这些限制严格地限制了程序可以执行的计算types。 (在实践中,在理论上,你可以通过在Fortran中模拟dynamic数据结构,将它们表示为一个大arrays,这是可怕的…而与此讨论无关)。

  2. 该语言可以提供一个明确的freedispose机制。 但是这依靠程序员来做对。 存储pipe理中的任何错误都可能导致内存泄漏……或者更糟的情况。

  3. 语言(或更严格的说是语言实现)可以为dynamic分配的存储提供自动存储pipe理器; 即某种forms的垃圾收集器。

唯一的其他select是永不回收dynamic分配的存储。 除了执行小计算的小程序之外,这不是一个实际的解决scheme。

把这个应用到Haskell中,语言没有1的限制,并且没有按照2的手动解除分配操作。因此,为了可用于非平凡的事情,Haskell实现需要包括垃圾收集器。

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

推测你的意思是一个纯粹的function语言。

答案是,在引擎盖下需要GC来回收语言必须创build的堆对象。 例如。

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

  • 有可能是周期的事实(例如由let rec产生)意味着引用计数方法不适用于堆对象。

  • 然后有函数闭包……也不能在堆栈上分配,因为它们的生命周期(通常)是独立于创build它们的堆栈帧的。

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

几乎所有包含闭包或graphics数据结构的示例都会在这些条件下泄漏。

如果你有足够的内存,垃圾收集器是不必要的。 但实际上,我们没有无限的记忆,所以我们需要一些方法来回收不再需要的内存。 在像C这样的不纯的语言中,你可以明确地声明你已经完成了一些内存来释放它 – 但是这是一个变异的操作(你刚才释放的内存不再安全),所以你不能使用这种方法纯粹的语言。 因此,要么静静地分析哪里可以释放内存(在一般情况下可能是不可能的),像筛子一样泄漏内存(很好,直到用完)或使用GC。

Haskell是一种非严格的编程语言,但大多数实现使用call-by-need(懒惰)来实现非严格性。 在需求调用中,只有在运行时使用“thunks”(expression式等待被评估,然后覆盖自己,保持可见值才能在需要时重用的expression式)的情况下,才会评估它。

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

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

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

编辑:我忘了。 懒惰是没有GC的地狱。 不要相信我? 只要在没有GC的情况下尝试一下,例如C ++。 你会看到…的东西