我好奇为什么Haskell实现要使用GC。
在纯函数式语言中,我想不出需要使用GC的情况。这只是一种优化以减少复制,还是确实有必要?
我正在寻找会泄漏的示例代码,如果没有GC存在。
我好奇为什么Haskell实现要使用GC。
在纯函数式语言中,我想不出需要使用GC的情况。这只是一种优化以减少复制,还是确实有必要?
我正在寻找会泄漏的示例代码,如果没有GC存在。
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]
必须保留在内存中,直到用户输入“清除”为止;因此,这个列表的生命周期需要动态确定,这就是动态内存管理的必要性所在。f :: Integer -> Integer
f x = let x2 = x*x in x2*x2
x2
安全释放时(而不是等待垃圾回收器释放 x2
),当 f
返回时。实质上,我们要求编译器执行逃逸分析,在可能的情况下将堆上的分配转换为栈上的分配。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
x2
从f
中逃逸(因为它在元组中返回),因此x2
必须在垃圾回收堆上分配。另一方面,区域推断能够检测到x2
在g
返回时可以被释放;这里的想法是x2
应该在g
的区域而不是f
的区域分配。
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在他们的区域推断工作中写了一篇相当易读的retrospective,其中大部分被整合到了MLKit compiler中。他们尝试了纯基于区域的内存管理(即没有垃圾收集器),以及混合的基于区域的/垃圾收集的内存管理,并报告说他们的测试程序运行速度比纯垃圾收集版本快了“10倍到4倍不等”。
让我们来看一个简单的例子。给定如下:
f (x, y)
在调用f
之前,您需要在某处分配一对 (x, y)
。那么什么时候才能解除该对的分配?你不知道。它不能在f
返回时被释放,因为f
可能已经将该对放入数据结构中(例如,f p = [p]
),因此该对的寿命可能比从f
返回的时间更长。现在,假设该对被放在了列表中,那么取出列表的任何人都不能释放该对。因为该对可能是共享的(例如,let p = (x, y) in (f p, p)
)。因此很难确定何时可以释放该对。
对于Haskell中的几乎所有分配,情况都是如此。 也就是说,有一种分析方法(区域分析)可以给出生命周期的上限。在严格语言中,这种方法运行得相当不错,但在惰性语言中效果较差(惰性语言在实现中往往会进行更多的变异)。
所以我想反过来问一个问题。您认为为什么Haskell不需要垃圾回收?您建议如何进行内存分配?
IO
。应用于Haskell的标准实现技术实际上比大多数其他语言都需要GC,因为它们从不改变先前的值,而是基于先前的值创建新的修改值。由于这意味着程序不断分配和使用更多内存,随着时间的推移,大量的值将被丢弃。
这就是为什么GHC程序往往具有如此高的总分配数字(从GB到TB):它们不断地分配内存,并且只有通过高效的GC才能在用尽之前收回内存。
您可能是指纯函数式语言。我想不出在纯语言中需要GC的情况。
接下来是函数闭包......它们也不能在堆栈上分配,因为它们的生命周期(通常)独立于创建它们的堆栈帧。
我正在寻找如果没有GC将会发生泄漏的示例代码。
几乎任何涉及闭包或图形数据结构的示例都会在这些条件下泄漏。
在纯函数式编程语言中,垃圾回收(GC)是必不可少的。为什么呢?因为操作alloc和free是不纯的!另一个原因是,不可变递归数据结构需要GC才能存在,因为反向链接会创建深奥且难以维护的结构,超出人类思维范畴。当然,反向链接是一种福音,因为使用它的结构的复制非常便宜。
无论如何,如果你不相信我,只需尝试实现FP语言,你就会发现我是正确的。
编辑:我忘了。在没有GC的情况下,惰性求值是地狱。不相信我吗?只需在例如C++中尝试一下没有GC的情况。你会看到...事情