Memory management Haskell编译器如何决定是在堆上还是堆栈上进行分配?

Memory management Haskell编译器如何决定是在堆上还是堆栈上进行分配?,memory-management,compiler-construction,haskell,stack,heap,Memory Management,Compiler Construction,Haskell,Stack,Heap,Haskell没有显式内存管理功能,所有对象都是按值传递的,因此也没有明显的引用计数或垃圾收集。Haskell编译器通常如何决定是生成在堆栈上分配的代码,还是生成在堆上为给定变量分配的代码?它是否会在同一函数的不同调用站点上一致地堆或堆栈分配相同的变量?当它分配内存时,它如何决定何时释放内存?堆栈分配和解除分配是否仍以与C相同的函数入口/出口模式执行?GHC在堆栈上放置的唯一内容是评估上下文。使用let/where绑定分配的任何内容以及所有数据构造函数和函数都存储在堆中。延迟求值使得您所知道的关

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

GHC在堆栈上放置的唯一内容是评估上下文。使用let/where绑定分配的任何内容以及所有数据构造函数和函数都存储在堆中。延迟求值使得您所知道的关于严格语言中执行策略的所有信息都变得无关紧要。

当您调用这样的函数时

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,例如我们示例中的(
gxy
/
p2
)。如果没有优化,这种执行模型效率很低,但是有很多方法可以避免这些开销

  • GHC做了大量的内联和拆箱工作。内联消除了函数调用的开销,并经常支持进一步的优化。取消装箱意味着更改调用约定,在上面的示例中,我们可以直接传递
    42
    ,而不是创建堆对象
    p1

  • 严格性分析发现是否保证对参数进行评估。在这种情况下,我们不需要创建thunk,而是完全计算表达式,然后将最终结果作为参数传递

  • 缓存小对象(当前只有8位
    Char
    s和
    Int
    s)。也就是说,不是为每个对象分配一个新指针,而是返回一个指向缓存对象的指针。即使对象最初是在堆上分配的,垃圾收集器稍后也会对其进行重复数据消除(只有较小的
    Int
    s和
    Char
    s)。因为对象是不可变的,所以这是安全的

  • 有限逃逸分析。对于局部函数,可能会在堆栈上传递一些参数,因为在外部函数返回时,它们已被视为死代码


  • 编辑:有关更多信息,请参阅。本文使用“push/enter”作为调用约定。较新版本的GHC使用“eval/apply”调用约定。有关切换的权衡和原因的讨论,请参见

    ,因为所有值无论如何都是不可变的,您不会注意到Haskell编译器是否优化了复制,而是在多个位置使用指向相同值的指针。事实上,至少GHC做了这种优化,在Haskell中说“所有对象都是按值传递的”是不太正确的。在Haskell中,引用透明是强制的——这意味着按值传递和按引用传递在所有情况下都会产生相同的结果。这允许编译器根据具体情况决定是通过引用还是通过值传递特定参数。结构成员也是如此。我想可能是这样的,这就是为什么我要问,跨呼叫站点是否保证相同。更好的说法是:既然编译器似乎可以自由选择何时进行堆栈分配和何时进行堆分配,那么给定的函数是否总是具有相同的调用约定?诺米诺的回答让我觉得不,你确定吗?正如nominolo所提到的,借助Escape分析,如果我们知道调用完成后不再需要某些对象,则可以在堆栈上分配它们。+1这不是一个坚如磐石的答案,但几乎是真的。Haskell是关于用功能性/声明性的思维方式来制作程序的;你(有点)“不应该”担心编译器是如何实现它的。什么是计算上下文?@DanBurton:必须有人来实现编译器;)既然您提到了转义分析:如果
    x
    未在其他任何地方使用,是否可以优化
    x++y
    以避免复制
    x
    (只需将最后一个尾部指针更改为指向
    y
    )?GHC会这样做吗?@Roman:不会,GHC的转义分析只适用于本地
    let
    -绑定。没有进行过程间分析。对于您的示例,您需要静态地确保堆中没有其他指针指向
    x
    或其任何后续指针。您需要线性或唯一性类型来证明类似的东西。由于优化,这是否意味着每个函数都可能有多个调用约定?例如,我意识到,如果函数是内联的,它实际上不再有调用约定,它可能会在其内联的函数中被任意更改,但如果说该函数是从另一个编译单元调用的(因此它将无法访问该定义,也无法内联该函数),函数是否具有一致的调用约定?还是GHC生成每个函数的多个版本以覆盖其基础或@约瑟夫:一个函数只能有一个调用约定。但是,编译器可能会生成其他函数。一种常见的技术是Worker/Wrapper转换,其中递归函数转换为非递归Wrapper和使用非常有效的调用约定的递归Worker。如果包装器是在调用站点内联的,那么代码将自动调用效率更高的worker(使用更快的调用约定)。如果函数是(1)递归的,并且(2)prova