Haskell需要垃圾收集器吗?

Pub*_*bby 113 garbage-collection haskell

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

我想不出一个纯语言需要GC的情况.它只是减少复制的优化,还是实际上是必要的?

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

rei*_*erp 215

正如其他人已经指出的那样,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
Run Code Online (Sandbox Code Playgroud)

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

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

然而...

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

f :: Integer -> Integer
f x = let x2 = x*x in x2*x2
Run Code Online (Sandbox Code Playgroud)

我们可能希望编译器检测到返回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
Run Code Online (Sandbox Code Playgroud)

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

超越Haskell

虽然如上所述,区域推理在某些情况下是有帮助的,但似乎很难有效地与懒惰评估相协调(参见Edward KmettSimon Peyton Jones的评论).例如,考虑一下

f :: Integer -> Integer
f n = product [1..n]
Run Code Online (Sandbox Code Playgroud)

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

在20世纪90年代和21世纪初,在严格的功能语言ML的区域推断方面进行了大量的工作.Mads Tofte,Lars Birkedal,Martin Elsman,Niels Hallenberg撰写了一篇关于区域推理工作的可读性回顾展,其中很多都集成到了MLKit编译器中.他们尝试了纯粹的基于区域的内存管理(即没有垃圾收集器)以及基于混合区域/垃圾收集的内存管理,并报告说他们的测试程序比纯垃圾运行"快10倍,慢4倍" - 收集版本.

  • 我真的很喜欢这个答案,尽管我唯一的困惑是第一个例子.显然,如果用户从未键入"清除",那么它可以使用无限内存(没有GC),但这并不是因为内存仍在被跟踪的漏洞. (3认同)
  • C++ 11有一个很棒的智能指针实现.基本上它采用引用计数.我猜Haskell可能会抛弃垃圾收集,转而采用类似的东西,因此变得具有确定性. (3认同)
  • @ChrisNash - 不起作用.智能指针使用引擎盖下的引用计数.引用计数不能处理具有周期的数据结构.Haskell可以生成带循环的数据结构. (3认同)
  • 我不确定我是否同意这个答案的动态内存分配部分.仅仅因为程序不知道用户何时暂时停止循环不应该使其动态化.这取决于编译器是否知道某些内容是否会脱离上下文.在Haskell的案例中,语言语法本身正式定义了它,生命语境是已知的.但是,由于列表表达式和类型是在语言中动态生成的,因此内存可能仍然是动态的. (3认同)
  • Haskell需要共享吗?如果没有,在你的第一个例子中,你可以将列表的副本(分别为"Nothing")传递给`loop`的递归调用并释放旧的 - 没有未知的生命周期.当然没有人想要Haskell的非共享实现,因为它对于大型数据结构来说非常慢. (2认同)
  • @StephenC - 智能指针可以很好地处理周期.Cycles只是关于如何链接数据,而不是如何处理内存.GC在大多数情况下都比较好,但是有一个SP选项会很好.有关周期的信息,请参阅此处:http://books.google.co.uk/books?id = fQVZy1zcpJkC&pg = SA40-PA14&lpg = SA40-PA14&div =数据+结构+with+cycles&source=bl&ots=aEumRlKtvL&sig=chT7kqiGAEldTenIpT5PCcjLY9A&hl=en&sa=X&ei = 0qD1UdPnL6md7Qb71ICoCA&VED = 0CEgQ6AEwBw (2认同)

aug*_*tss 26

让我们举一个小例子.鉴于这种

f (x, y)
Run Code Online (Sandbox Code Playgroud)

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

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

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


sig*_*fpe 16

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

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

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

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

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

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

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

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

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

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

  • 自从我写了这个答案以来,Rust 编程语言的流行度增加了很多。所以值得一提的是,Rust 使用线性类型系统来控制对内存的访问,如果你想看看我提到的在实践中使用的想法,值得一看。 (3认同)

ehi*_*ird 14

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

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

  • "他们永远不会改变以前的价值":你可以查看http://www.haskell.org/haskellwiki/HaskellImplementorsWorkshop/2011/Takano,它是关于重用内存的实验性GHC扩展. (2认同)

Ste*_*n C 11

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • 为什么您认为您的选项列表是详尽无遗的?目标C中的ARC,MLKit和DDC中的区域推断,Mercury中的编译时垃圾收集 - 它们都不适合此列表. (2认同)
  • @ChrisNash-1)无效。如果存在循环,则基于引用计数的回收将泄漏数据...除非您可以依靠应用程序代码来中断循环。2)众所周知(对于研究这些问题的人而言),与现代(真实)垃圾收集器相比,引用计数的性能较差。 (2认同)

bdo*_*lan 8

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

  • 如果理论上GC在一般情况下是不必要的,那么从理论上说它对Haskell来说也是不必要的. (10认同)