何时在 lisp 解释器中释放闭包的内存

when to free a closure's memory in a lisp interpreter

我正在从头开始编写一个简单的 lisp 解释器。我有一个全局环境,在评估文件中的所有表单时绑定了顶级变量。当文件中的所有表单都被评估后,顶级 env 和其中的所有键值数据结构都被释放。

当求值器遇到 lambda 表单时,它会创建一个 PROC 对象,其中包含 3 个内容:应用程序时要绑定到本地框架中的参数列表,正文函数的名称,以及指向创建它的环境的指针。例如:

(lambda (x) x)

会在内部产生类似的东西:

PROC- args: x, 
      body: x, 
      env: pointer to top level env

应用 PROC 时,会为框架创建一个新环境,并在那里暂存本地绑定,以允许使用适当的绑定评估主体。此框架环境包含指向其闭包的指针,以允许在 THAT 内部进行变量查找。在这种情况下,这将是全球环境。在评估 PROC 主体后,我可以释放与其关联的所有单元格,包括它的框架环境,然后在没有内存泄漏的情况下退出。

我的问题是高阶函数。考虑一下:

(define conser 
    (lambda (x)
        (lambda (y) (cons x y))))

一个接受一个参数并产生另一个函数的函数,该函数将把该参数转换为您传递给它的东西。所以,

(define aconser (conser '(1)))

将产生一个函数,该函数将 '(1) 传递给它的任何内容。例如:

(aconser '(2)) ; ((1) 2) 

我的问题是 aconser 必须保留指向其创建环境的指针,即通过调用 (conser '(1)) 生成时 conser 的指针。当 aconser 应用 PROC 时,它的框架必须指向定义 aconser 时存在的 conser 框架,所以我无法释放 [= 的框架21=] 应用后。我不知道 how/the 在应用时释放与 lambda 框架关联的内存并支持这种持久高阶函数的最佳方法。

我能想到一些解决办法:

这似乎是隐含的意思here。因此,我不是将 PROC 对象中的指针保存到它的闭包中,而是...复制闭包环境并直接在单元格中存储指向 that 的指针?这不只是将罐头踢得更深一层并导致同样的问题吗?

我担心我可能在这里遗漏了一些非常简单的东西,而且我也很好奇 lisp 的其他实现和其他通常带有闭包的语言如何支持此过程。我没有太多运气寻找答案,因为这个问题非常具体,甚至可能是这个实现(我承认我只是作为一个学习项目脱颖而出)而且我能找到的大部分内容都只是解释了细节从正在实施的语言的角度来看闭包,而不是从正在实施的语言的语言来看。

Here is a link 到我源代码中的相关行,如果有帮助的话,如果这个问题不够详细,无法彻底描述问题,我很乐意详细说明。谢谢!

通常在天真的解释器中处理这个问题的方法是使用垃圾收集器 (GC) 并在 GC 堆中分配你的激活帧。因此,您永远不会明确释放这些帧,而是让 GC 在适用时释放它们。

在更复杂的实现中,您可以使用稍微不同的方法:

  • 创建闭包时,不要存储指向当前环境的指针。相反,复制闭包使用的那些变量的值(它称为 lambda 的 自由变量 )。 并更改闭包的主体以使用这些副本,而不是在环境中查找这些变量。这叫做闭包转换
  • 现在您可以将您的环境视为普通堆栈,并在退出范围后立即释放激活帧。
  • 您仍然需要 GC 来决定何时可以释放 闭包
  • 这又需要一个 "assignment conversion":复制变量的值意味着如果这些变量被修改,语义也会发生变化。所以要恢复原来的语义,你需要寻找那些 "copied into a closure" 以及 "modified" 的变量,并将它们变成 "reference cells" (例如 cons 将值保存在 car) 中的单元格,以便副本不再复制该值,而只是复制对保存该值的实际位置的引用。 [旁注:这样的实现显然意味着避免 setq 并使用更实用的样式可能最终会更有效率。 ]

更复杂的实现还有一个优点是它可以为 space 语义提供 安全:闭包将只保存它实际引用的数据,与闭包最终引用整个周围环境的天真方法相反,因此可以防止 GC 收集实际上未引用但恰好在闭包捕获时恰好在环境中的数据。