Haskell没有显式内存管理功能,所有对象都是按值传递的,所以也没有明显的引用计数或垃圾回收。 Haskell编译器通常如何决定是否生成在堆栈上分配的代码与在堆上为给定变量分配的代码?是否一致堆或堆栈为同一个函数在不同的调用站点分配相同的变量?当它分配时,它如何决定何时释放内存?堆栈分配和解除分配是否仍在与C中相同的函数入口/出口模式中执行?
答案 0 :(得分:33)
当你调用这样的函数时
f 42 (g x y)
然后运行时行为类似于以下内容:
p1 = malloc(2 * sizeof(Word))
p1[0] = &Tag_for_Int
p1[1] = 42
p2 = malloc(3 * sizeof(Word))
p2[0] = &Code_for_g_x_y
p2[1] = x
p2[2] = y
f(p1, p2)
也就是说,参数通常作为指向堆上对象的指针传递,就像在Java中一样,但与Java不同,这些对象可能代表暂停的计算,即 thunks ,例如(g x y
/ p2
}在我们的例子中。如果没有优化,这种执行模型的效率非常低,但有一些方法可以避免许多这些开销。
GHC做了很多内联和拆箱。内联消除了函数调用开销,并且通常可以进一步优化。取消装箱意味着更改调用约定,在上面的示例中,我们可以直接传递42
,而不是创建堆对象p1
。
严格性分析发现是否保证可以评估参数。在这种情况下,我们不需要创建thunk,而是完全评估表达式,然后将最终结果作为参数传递。
缓存小对象(目前只有8位Char
和Int
)。 也就是说,不是为每个对象分配一个新指针,而是返回一个指向缓存对象的指针。即使该对象最初是在堆上分配的,垃圾收集器也会在以后对它们进行重复处理(只有小Int
和Char
s。由于对象是不可变的,因此这是安全的。
有限逃脱分析。对于本地函数,可以在堆栈上传递一些参数,因为在外部函数返回时已知它们是死代码。
修改:有关(更多)更多信息,请参阅"Implementing Lazy Functional Languages on Stock Hardware: The Spineless Tagless G-machine"。本文使用“push / enter”作为调用约定。较新版本的GHC使用“eval / apply”调用约定。有关该转换的权衡和原因的讨论,请参阅"How to make a fast curry: push/enter vs eval/apply"
答案 1 :(得分:2)
GHC放在堆栈上的唯一事情是评估上下文。使用let / where绑定分配的任何内容以及所有数据构造函数和函数都存储在堆中。懒惰的评估使您对严格语言中执行策略的了解无关紧要。