正如其他人已经指出的,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]必须保存在内存中,直到用户输入“clear”;所以这个的生命周期必须动态确定,这就是为什么动态内存管理是必要的。
所以从这个意义上说,自动动态内存分配是必要的,在实践中这意味着:是的,Haskell 需要一个垃圾收集器,因为垃圾收集是性能最高的自动动态内存管理器。
但是...
虽然垃圾收集器是必要的,但我们可能会尝试找到一些特殊情况,编译器可以使用比垃圾收集更便宜的内存管理方案。例如,给定
f :: Integer -> Integer
f x = let x2 = x*x in x2*x2
我们可能希望编译器检测到当f 返回时可以安全地释放x2(而不是等待垃圾收集器释放x2)。本质上,我们要求编译器执行escape analysis 以尽可能将分配到垃圾收集堆转换为allocations on the stack。
这不是太不合理的要求:jhc haskell compiler 这样做,尽管 GHC 没有。 Simon Marlow says 认为 GHC 的分代垃圾收集器使得逃逸分析几乎没有必要。
jhc 实际上使用了一种复杂的转义分析形式,称为region inference。考虑
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的区域。
超越 Haskell
虽然区域推断在上述某些情况下很有帮助,但似乎很难与惰性求值有效协调(参见Edward Kmett's 和Simon Peyton Jones' cmets)。例如,考虑
f :: Integer -> Integer
f n = product [1..n]
人们可能会想在堆栈上分配列表[1..n],并在f 返回后释放它,但这将是灾难性的:它会改变f 使用O(1) 内存(在垃圾收集下)到 O(n) 内存。
在 1990 年代和 2000 年代初期,针对 strict 功能语言 ML 的区域推断进行了大量工作。 Mads Tofte、Lars Birkedal、Martin Elsman、Niels Hallenberg 就他们在区域推断方面的工作写了一个可读性很强的retrospective,其中大部分都集成到了MLKit compiler。他们试验了纯基于区域的内存管理(即没有垃圾收集器)以及混合的基于区域/垃圾收集的内存管理,并报告说他们的测试程序比纯垃圾运行“快 10 倍和慢 4 倍”-收集的版本。