通常如何为闭包实现引用环境?

时间:2012-09-02 03:18:10

标签: compiler-construction programming-languages closures

假设我有一个带有深度绑定的静态/词法范围的语言,我创建了一个闭包。闭包将包含我想要执行的语句加上所谓的引用环境,或引用this post,可以使用的变量集合。

这个引用环境实际上看起来像实现方式?我最近在阅读有关ObjectiveC的块实现的文章,作者建议在幕后获得堆栈上所有变量的副本以及堆对象的所有引用。该解释声称您在关闭创建的时间点获得了引用环境的“快照”。

  1. 或多或少会发生什么,或者我误解了?
  2. 做了什么来“冻结”堆对象的单独副本,或者可以安全地假设如果它们在闭包创建和闭包执行之间被修改,则闭包将不再在原始版本上运行对象
  3. 如果确实存在复制,那么在人们可能想要创建大量闭包并将其存储在某处的情况下是否存在内存使用注意事项?
  4. 我认为误解其中一些概念可能会导致棘手的问题,例如Eric Lippert在this blog post中提到的问题。这很有意思,因为你认为保持对一个值类型的引用没有意义,这个值类型可能在调用闭包时消失,但我猜测在C#中编译器会发现变量稍后需要将它放入堆中。

    似乎在大多数内存管理语言中,一切都是引用,因此ObjectiveC是一种独特的情况,必须处理复制堆栈中的内容。

2 个答案:

答案 0 :(得分:3)

这是一个类似伪javascript语法的运行示例。

function f(x) {
  var y = ...;
  function g(z) {
    function h(w) {
      .... y, z, w ....
    }
    .... x, h ....
  }
  .... x, g ....
}

一种表示是一种链接的环境链。也就是说,闭包由一个代码指针,一些槽和一个封闭闭包或顶层环境的引用组成。在此表示中,

f = [<code>, <top-level-env>]
g = [<code>, f, x, y]
h = [<code>, g, z]

除非有时最好让每个函数都直接引用顶层环境,因为它经常被使用:

f = [<code>, <top-level-env>]
g = [<code>, <top-level-env>, f, x, y]
h = [<code>, <top-level-env>, g, z]

(还有其他变种。)

这种表示的一个优点是你可以在闭包中存储可变变量。 (好吧,也许,取决于你如何表示函数激活。)一个缺点是,如果你有深度嵌套的闭包,一些变量可能需要多次跳转才能达到。另一个缺点是,如果一个闭包超过其父级(例如,g返回h),那么这种表示可能会阻止GC收集大部分甚至完全无法访问的环境帧。

另一种表示是“平面闭包”:每个闭包包含一个代码指针和所有代码自由变量的槽。

g = [<code>, x, y]
h = [<code>, y, z]

此表示修复了空间/ GC问题;没有关闭引脚在内存中另一个关闭。另一方面,自由变量槽被复制而不是共享,所以如果有一个带有许多自由变量的嵌套闭包---或嵌套闭包的许多实例 - 整体内存使用可能会更高。此外,这种表示通常要求存储可变变量以进行堆分配(但仅适用于实际变异的变量,并且仅在不能自动重写变异时)。

还有混合方法。例如,您可能拥有大多数平坦的闭包,但特别对待顶级环境:

g = [<code>, <top-level-env>, x, y]

或者你可能有一个“足够聪明”(或至少“足够雄心勃勃”)的编译器,它试图根据自由变量的数量,嵌套深度等来选择表示。

答案 1 :(得分:2)

在Smalltalk中,闭包可以引用“外部上下文”。外部上下文通常是创建闭包的方法的堆栈框架,但对于嵌套闭包,它可能是另一个闭包。

持有对外部上下文的引用的闭包很昂贵,因为(我猜)它们会阻止相应的堆栈被垃圾收集。因此,闭包仅在真正需要时引用外部上下文:

清理闭包:没有引用任何本地的闭包。他们不需要引用外部背景。

E.g。 [ Transcript show: 'something' ]

复制闭包:闭包,引用变量,在创建闭包后不会更改。创建闭包时变量的值将复制到闭包本身中。然后,不需要保持对外部上下文的引用。例如。

  

|清单|
      list:= OrderedCollection new。
      1:5做:[:i   | list add:i]。

完全闭包:保留对外部上下文的引用的闭包。 E.g。

  

|计数器|
      counter:= 0.
      1:5做:[:i   | counter:= counter + 1]。

如果关闭的变量在创建闭包后发生变异,则需要完全闭包,但非本地返回也是如此。关于非本地回报,您可能会喜欢blog post from Neal Gafter

关于即将到来的JDK 7的关闭,Brian Goetz'State of the Lambda的好读也是如此。我发现有趣的讨论为什么他们会坚持使用Java limation来捕获最终的最终变量并禁止捕获可变局部变量。上面的完全关闭示例将不受支持。他们声称的论点是,它主要是连续成语。