Haskell编译器如何决定在堆上还是栈上分配内存?

32

Haskell没有显式的内存管理,所有对象都是按值传递的,因此没有明显的引用计数或垃圾回收。 Haskell编译器通常如何决定为给定变量生成在堆栈上分配的代码还是在堆上分配的代码? 它是否会在相同函数的不同调用站点中始终堆或栈分配相同的变量? 在分配时,它如何决定何时释放内存? 栈分配和解除分配是否仍按C语言中相同的函数入口/出口模式执行?


由于所有值都是不可变的,因此如果Haskell编译器优化了复制并使用指针指向多个位置的同一值,您甚至可能都没有注意到。事实上,至少GHC进行了这种优化。 - user395760
15
在Haskell中,说“所有的对象都是按值传递”并不完全正确。在Haskell中,强制执行参照透明性——这意味着按值传递和按引用传递在所有情况下产生相同的结果。这使得编译器可以根据情况决定是否按引用或按值传递特定参数。结构成员也是如此。 - Dietrich Epp
1
我想这可能是情况,这就是为什么我问是否保证在所有调用点中都是一样的。更好的说法应该是:由于编译器似乎有自由选择何时进行栈分配和堆分配的权利,一个给定的函数是否总是遵循相同的调用约定?nominolo的回答让我认为不会。 - Joseph Garvin
2个回答

38

当您像这样调用一个函数时

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)。如果没有优化,这种执行模型效率非常低,但有方法可以避免许多这些开销。
  1. GHC进行了大量的内联和展开。内联消除了函数调用的开销,并经常启用进一步的优化。展开意味着改变调用约定,在上面的例子中,我们可以直接传递42而不是创建堆对象p1
  2. 严格性分析找出参数是否保证已求值。 在这种情况下,我们不需要创建一个thunk,而是完全评估表达式,然后将最终结果作为参数传递。
  3. 小对象(目前仅为8位CharInt)被缓存。即,返回对缓存对象的指针,而不是为每个对象分配一个新指针。即使对象最初在堆上分配,垃圾收集器以后也会去重它们(仅限于小的IntChar)。由于对象是不可变的,因此这是安全的。
  4. 有限逃逸分析。 对于局部函数,一些参数可以通过堆栈传递,因为它们在外部函数返回时已知是死代码。
编辑:有关更多信息,请参见“在股票硬件上实现惰性函数语言:无脊椎标签G机”的论文。 本论文使用“push / enter”作为调用约定。 GHC的新版本使用“eval / apply”调用约定。 有关该开关的权衡和原因的讨论,请参见“如何制作快速咖喱:push / enter vs eval / apply”的论文

1
既然你提到了逃逸分析:如果 x 没有在其他地方使用,那么优化 x ++ y 以避免复制 x(只需将最后的尾指针更改为指向 y)是否可能? GHC 是否会这样做? - Roman Cheplyaka
@Roman:不,GHC的逃逸分析仅适用于本地的let绑定。没有进行任何过程间分析。对于您的示例,您需要静态确保堆中没有其他指针指向x或其任何后继节点。您需要线性或唯一类型来证明这样的事情。 - nominolo
2
由于优化,这是否意味着每个函数可能有多个调用约定?我意识到例如如果函数被内联,它实际上不再具有调用约定,并且它可能会在内联到的函数中任意更改,但是如果说该函数从另一个编译单元中调用(因此无法访问定义并且无法内联该函数),那么该函数是否具有一致的调用约定?或者GHC是否生成多个版本的每个函数来覆盖其基础或...? - Joseph Garvin
1
另一个很好的参考资料是 GHC Wiki:http://hackage.haskell.org/trac/ghc/wiki/Commentary/Rts/HaskellExecution/FunctionCalls - sclv
@nominolo 我认为GHC在内部使用或模拟唯一类型实际上可能是一个好主意(不一定要将它们添加到语言中),以便确定何时可以安全地就地修改数据。 - Jeremy List
显示剩余2条评论

2

GHC只在堆栈上放置评估上下文。任何使用let/where绑定分配的东西以及所有数据构造函数和函数都存储在堆中。惰性求值使得你所知道的关于严格语言执行策略的一切都变得无关紧要。


你确定吗?正如nominolo所述,借助逃逸分析的帮助下,如果我们知道某些对象在调用完成后不再需要,它们可以分配到堆栈上。 - Matthieu M.
+1 这不是一个非常确定的答案,但它有一定的道理。Haskell 是关于使用函数式/声明式思维方式来编写程序;你(在某种程度上)“不应该”担心编译器实际上如何实现它。 - Dan Burton
2
什么是评估上下文? - MasterMastic
1
@DanBurton:得有人实现编译器;) - Joseph Garvin

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