我很好奇为什么Haskell实现使用GC。
我想不出用纯语言来进行GC的必要性。仅仅是减少复制的优化,还是实际上有必要?
我正在寻找示例代码,如果不存在GC,这些代码可能会泄漏。
我很好奇为什么Haskell实现使用GC。
我想不出用纯语言来进行GC的必要性。仅仅是减少复制的优化,还是实际上有必要?
我正在寻找示例代码,如果不存在GC,这些代码可能会泄漏。
Answers:
正如其他人已经指出,哈斯克尔需要自动,动态内存管理:自动内存管理是必要的,因为手动内存管理是不安全的; 动态内存管理是必需的,因为对于某些程序,对象的生存期只能在运行时确定。
例如,考虑以下程序:
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需要使用垃圾收集器,因为垃圾收集是性能最高的自动动态内存管理器。
尽管垃圾收集器是必需的,但我们可能会尝试找到一些特殊情况,其中编译器可以使用比垃圾收集便宜的内存管理方案。例如,给定
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
在这种情况下,简单的转义分析将得出x2
转义来自f
(因为它在元组中返回)的结论,因此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倍”。收集的版本。
Nothing
)传递给的递归调用,loop
并取消分配旧列表-没有未知的生存期。当然,没有人想要非共享的Haskell实现,因为对于大型数据结构而言,它的运行速度极其慢。
让我们举一个简单的例子。鉴于这种
f (x, y)
您需要(x, y)
在致电之前在某处分配该对f
。您什么时候可以取消分配那对?你不知道。f
返回时无法将其释放,因为f
可能已将该对放入数据结构中(例如f p = [p]
),因此该对的生存期可能必须长于from的返回f
。现在,假设该对已放入列表中,谁能将列表拆开,谁能取消分配该对?否,因为该对可能是共享的(例如let p = (x, y) in (f p, p)
)。因此,很难分辨何时可以解除分配。
Haskell中的几乎所有分配都适用。就是说,有可能进行分析(区域分析)以给出生命周期的上限。这在严格的语言中相当有效,但在懒惰的语言中却不太好(在实现中,懒惰的语言比严格的语言所做的变异要多得多)。
所以我想解决这个问题。为什么您认为Haskell不需要GC。您如何建议进行内存分配?
您的直觉与纯真有关,这有一定道理。
Haskell被认为是纯粹的,部分原因是在类型签名中考虑了函数的副作用。因此,如果函数具有打印某些内容的副作用,则必须有一个IO
它的返回类型中地方。
但是在Haskell的每个地方都有一个隐式使用的函数,它的类型签名不能解释某种意义上的副作用。即复制一些数据并给您两个版本的函数。实际上,这可以通过复制内存中的数据从字面上进行工作,也可以通过增加需要稍后偿还的债务来“虚拟地”进行。
可以使用限制更大的类型系统(纯粹是“线性”系统)来设计语言,这些系统不允许使用复制功能。从使用这种语言的程序员的角度来看,Haskell看起来有些不纯。
实际上,Clean(是Haskell的亲戚)具有线性(更严格地说是唯一)类型,这可以使您了解禁止复制的方式。但是Clean仍允许复制“非唯一”类型。
这个领域有很多研究,如果您对Google足够了解,就会发现不需要垃圾收集的纯线性代码示例。您会找到各种类型的系统,它们可以向编译器发出信号,表明可能使用了什么内存,从而使编译器可以消除某些GC。
从某种意义上说,量子算法也是纯线性的。每个操作都是可逆的,因此无法创建,复制任何数据或销毁。(它们在通常的数学意义上也是线性的。)
与Forth(或其他基于堆栈的语言)进行比较也很有趣,这些语言具有显式的DUP操作,可以清楚何时进行复制。
对此的另一种(更抽象的)思考方式是,注意Haskell是基于笛卡尔封闭类别理论的简单类型的lambda演算建立的,并且这些类别配备了对角函数diag :: X -> (X, X)
。基于另一类类别的语言可能没有这种东西。
但是总的来说,纯线性编程太难用了,因此我们选择使用GC。
实际上,应用于Haskell的标准实现技术比大多数其他语言更需要GC,因为它们从不改变以前的值,而是根据以前的值创建新的,修改后的值。由于这意味着程序会不断分配并使用更多的内存,因此随着时间的流逝,大量的值将被丢弃。
这就是为什么GHC程序倾向于具有如此高的总分配数字(从GB到TB)的原因:它们一直在分配内存,这仅归功于高效的GC在用尽之前回收了它。
如果一种语言(任何一种语言)都允许您动态分配对象,则有三种实用的方法来处理内存管理:
该语言只能允许您在堆栈上或启动时分配内存。但是这些限制严重限制了程序可以执行的计算类型。(在实践中。从理论上讲,您可以通过将Fortran动态数据结构以大数组表示来模拟动态数据结构。这是可怕的,与本讨论无关。)
语言可以提供显式free
或dispose
机制。但这依赖于程序员才能正确完成。存储管理中的任何错误都可能导致内存泄漏……甚至更糟。
语言(或更严格地说,是语言实现)可以为动态分配的存储提供自动存储管理器;即某种形式的垃圾收集器。
唯一的其他选择是从不回收动态分配的存储。除了执行少量计算的小型程序以外,这不是实际的解决方案。
将其应用于Haskell,该语言没有1.的限制,并且没有按照2进行的手动释放操作。因此,为了可用于非平凡的事物,Haskell实现需要包含垃圾回收器。
我想不出用纯语言需要GC的情况。
想必您是指纯功能语言。
答案是在后台需要GC来回收该语言必须创建的堆对象。例如。
一个纯函数需要创建堆对象,因为在某些情况下它必须返回它们。这意味着它们不能在堆栈上分配。
可能存在循环的事实(let rec
例如,产生于a )意味着引用计数方法不适用于堆对象。
然后是函数闭包...也不能在堆栈上分配,因为它们的生存期(通常)与创建它们的堆栈框架无关。
我正在寻找示例代码,如果不存在GC,这些代码可能会泄漏。
在这种情况下,几乎所有涉及闭包或图形数据结构的示例都会泄漏。
只要您有足够的内存,就永远不需要垃圾收集器。但是,实际上,我们没有无限的内存,因此我们需要某种方法来回收不再需要的内存。在诸如C之类的不纯语言中,您可以明确声明已完成一些内存释放操作-但这是一个变异操作(刚释放的内存不再安全读取),因此您不能在这种情况下使用此方法一种纯净的语言。因此,它要么以某种方式静态分析了可以释放内存的位置(在一般情况下可能是不可能的),像筛子一样的泄漏内存(在用完之前可以很好地工作)或使用GC。
Haskell是一种非严格的编程语言,但是大多数实现都使用按需调用(惰性)来实现非严格性。在按需调用中,您仅在运行时使用“ thunk”机制评估事物(表示等待评估然后覆盖自身的表达式,在需要时可重复使用的值保持可见)。
因此,如果您使用thunk懒惰地实现语言,则将有关对象生存期的所有推理推迟到最后一个时刻,即运行时。由于您现在对生命周期一无所知,因此您唯一可以合理做的就是垃圾回收...