为什么Haskell编译器不支持确定性内存管理?

22

鉴于Haskell运行时可以获得大量类型信息,为什么不能避免运行GC进行清理呢?在编译代码时,应该能够找出所有用法并插入适当的alloc/release调用,对吧?这将避免运行时GC的开销。

5个回答

25

有理由质疑函数式编程语言是否能通过跟踪使用情况来减少垃圾回收。尽管由于条件分支而导致某些数据是否可以安全丢弃的一般问题是不可判定的,但是静态工作更加可行,并且可以找到更多直接释放内存的机会。

值得注意的是,Martin Hofmann和移动资源保证项目团队在类型导向的内存(de/re)分配方面做出了重大贡献。然而,使他们的东西起作用的是Haskell在其类型系统中没有的东西——线性性。如果您知道函数的输入数据对于计算的其他部分是“秘密”的,那么您可以重新分配它们占用的内存。 MRG的特别好处在于,它管理一个实际的兑换率,将一种类型的释放与另一种类型的分配联系起来,这在纯函数外表下变成了老式的指针混淆。事实上,许多可爱的简化变异算法(例如指针反转遍历,覆盖尾指针构造等)可以使用这些技术变得纯函数化(并检查nasty bugs)。

实际上,资源的线性类型给出了对可能有助于减少GC的使用分析的保守但机械可检查的近似。有趣的问题包括如何将这种处理与通常的持久交易清理混合在一起(故意副词选择)。在递归计算中,似乎有相当多的中间数据结构具有初始的单线程阶段,然后在计算完成时被共享或丢弃。 可能可以减少此类过程生成的垃圾。

简而言之,有良好的类型化使用分析方法可以减少GC,但是Haskell现在具有不适合特别实用此目的的类型信息。


2
线性与懒惰并不相容。 - Alexandre C.
6
你断言了这一点,但你没有为我提供自己获得这种确定性的基础。你可能是对的,但你是否愿意详细说明导致你得出这个结论的问题呢?我知道Clean的独特类型不完全是线性类型系统,但Clean是惰性求值的。同时,一些异端者建议Haskell在其惰性方面应更明确地界定,这可能为其他类型的约束留下空间。我想在这里仍然存在怀疑的余地,以及与之相伴随的创新。 - pigworker
2
嗯,我猜这里选择“纪律”一词的方式与选择“干净”的方式一样有意。就个人而言,我更愿意将递归代码与核心递归代码区分开来,而不是将严格代码与惰性代码区分开来,但这并不重要。对于Haskell更为直接相关的是,我想知道是否可以使用高阶类型a la runST来进行某种形式的作用域分配。虽然这与通过代码分析推断这些属性的相反! - C. A. McCann
1
一个良好的严格与惰性分离可能需要一个良好的递归与核递归分离。runST技巧可能让我们堆叠分配区域,但并不明显堆叠是我们唯一需要的结构。在这个充满扩散互联处理器的新世界中,我们确实可以使用类型来说明东西在哪里(以及何时!)以及它是什么。我们可以想象有一个固定寿命数据区域,在那里分配的所有东西,如果没有明确导出到其他地方,就会在最后被压缩。有趣的时代在前方! - pigworker
什么?我能有这篇MRG论文的链接吗? - noɥʇʎԀʎzɐɹƆ

14

基于区域的内存管理是C和C ++程序员经常手动编程的内容:分配一块内存(“区域”,“arena”等),在其中分配单个数据,使用它们,并在不再需要任何单个数据时释放整个块。上世纪90年代,Tofte、Aiken和其他人(包括我和我们各自的同事)的工作表明,可以通过“区域推断”静态地自动推断出区域分配和释放点,以保证块从未过早地被释放,在实践中足够早地避免长时间持有最后一个数据后释放大量内存。例如,ML Kit for Regions是基于区域推断的完整标准ML编译器。在其最终版本中,它与内部区域垃圾收集相结合:如果静态推断显示存在长期使用的区域,则在其中使用垃圾收集。你可以同时拥有你的蛋糕并吃掉它:对于长期存活的数据,你拥有垃圾收集,但大量数据像堆栈一样管理,即使通常会进入堆。


8

请考虑以下伪代码:

func a = if some_computation a
    then a
    else 0

要知道在调用func a之后,a是否为垃圾,编译器必须能够知道some_computation a的结果。如果它可以在一般情况下做到这一点(这需要解决停机问题),那么根本不需要为此函数生成代码,更不用说垃圾收集了。类型信息是不足的。


这可能有点愚蠢,但我们不是已经知道在else分支中a是无用的吗?我们只需要在该分支中插入调用collect的语句即可。 - Asad-ullah Khan
如果周围还有其他对a的引用怎么办?例如,它是从某个具有对它的引用的地方传递进来的。我9年前写得不是很好,但重点是在运行y = func x之后,y可能是对值x的引用,也可能不是。您需要识别所有实际上引用x的东西,以知道在哪里静态编译调用以收集x,而不仅仅是所有可能引用x的东西。 - Ben

5

使用惰性求值确定对象的生存周期并不容易。JHC编译器具有(或曾经拥有)区域内存管理,试图通过在生命周期结束时进行解除分配来释放内存。

我也很好奇你所说的确定性内存管理是什么意思。


2
我猜他所说的“确定性”是指“积极”的内存管理,换句话说,“一旦不再需要就立即摆脱它”,而不是“只有在垃圾回收检测到不再需要时才摆脱它”。 - Dan Burton
一旦某些东西变得不必要,就将其丢弃是不可判定的。也许他的意思类似于引用计数,这是对此的近似。但它仍然不完全确定。 - augustss
1
引用计数是“引用透明”的,就某种意义而言,不是吗?这意味着,如果程序给出相同的输入,它应该在相同的时间执行解除分配。因此,它在很大程度上是确定性的。 - Dan Burton
@Dan 其他的垃圾回收方法同样是确定性的。在相同的输入下,释放内存将会在相同的时间发生。这在ghc中并不成立,因为rts使用了线程和计时器,但它并不必须这样做。 - augustss
@Dan,@augustss... “意思是,如果程序给出相同的输入,它应该在相同的时间执行解除分配”.. 是的,这就是我所说的确定性。由于rts使用线程,这就是为什么在ghc中不成立的原因。 - Pradeep

-1

类型信息主要与编译时有关,而内存管理是运行时的事情,因此我认为它们彼此之间没有关联。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接