如何在没有gc的情况下实现闭包?
我正在设计一种语言。首先,我想决定要生成什么代码。该语言将具有类似于javascript的词汇闭包和基于原型的继承。但是我不是gc的粉丝,所以尽量避免。那么问题来了:是否有一种优雅的方法来实现闭包而不需要在堆上分配堆栈框架并将其留给垃圾收集器?
我的第一个想法:
- 使用引用计数并垃圾收集周期(我不是很喜欢)
- 使用意大利面条堆栈(看起来效率很低)
- 将闭包的形成限制在某些上下文中,这样我就可以摆脱返回地址栈和本地人栈的困扰。
我不会使用高级语言或者遵循任何调用约定,因此我可以随意破坏堆栈。
(编辑:我知道引用计数是垃圾收集的一种形式,但是我使用gc的含义更为普遍)
解决方案
如果我们可以通过不使用GC来解释要避免的问题,那么这将是一个更好的问题。如我们所知,大多数提供词汇闭包的语言都会在堆上分配它们,并允许它们在创建它们的激活记录中保留对变量绑定的引用。
我知道这种方法的唯一替代方法是gcc
用于嵌套函数:为该函数创建一个蹦床并将其分配在堆栈上。但是正如gcc手册所述:
If you try to call the nested function through its address after the containing function has exited, all hell will break loose. If you try to call it after a containing scope level has exited, and if it refers to some of the variables that are no longer in scope, you may be lucky, but it's not wise to take the risk. If, however, the nested function does not refer to anything that has gone out of scope, you should be safe.
简短的版本是,我们有三个主要选择:
- 在堆栈上分配闭包,并且在包含函数退出后不允许使用它们。
- 在堆上分配闭包,并使用某种垃圾回收。
- 做原始的研究,可能是从ML,Cyclone等拥有的地区开始的。
如果我们拥有用于精确复制GC的机制,则可以在出口处发现指向该堆栈帧的指针已转义,从而可以在堆栈上进行初始分配并复制到堆并更新指针。这样,仅当我们确实捕获包含此堆栈框架的闭包时,我们才需要付费。这是有用还是有害取决于我们使用闭包的频率以及捕获的数量。
我们可能还会研究C ++ 0x的方法(N1968),尽管正如人们对C ++所期望的那样,它包括依靠程序员指定要复制的内容和要引用的内容,如果出错,则只会获得无效的访问权限。
还是根本不做GC。在某些情况下,最好只是忘记内存泄漏,并在完成后让进程进行清理。
根据我们对GC的看法,我们可能会担心定期进行GC扫描。在这种情况下,当项目超出范围或者指针更改时,我们可以执行选择性GC。我不确定这会花多少钱。
@艾伦
如果在包含函数退出时不能使用它们,闭包有什么好处?据我了解,这就是关闭的全部要点。
C ++ 0x规范定义了不带垃圾回收的lambda。简而言之,在lambda闭包包含不再有效的引用的情况下,该规范允许进行不确定的行为。例如(伪语法):
(int)=>int create_lambda(int a) { return { (int x) => x + a } } create_lambda(5)(4) // undefined result
此示例中的lambda引用在堆栈上分配的变量(a
)。但是,该堆栈框架已弹出,并且在函数返回后不一定可用。在这种情况下,它可能会工作并作为结果返回" 9"(假设编译器的语义是合理的),但是没有办法保证它。
如果我们要避免垃圾回收,那么我假设我们还允许显式堆与堆栈分配以及(可能)指针。如果是这种情况,那么我们可以像C ++一样,仅假设使用语言的开发人员将足够聪明,可以发现有lambda的问题案例并显式地复制到堆中(就像我们要返回的是在功能)。
Use reference counting and garbage collect the cycles (I don't really like this)
可以将语言设计为没有循环:如果我们只能创建新对象而不能对旧对象进行突变,并且如果使一个对象不能构成循环,则循环永远不会出现。尽管在实践中,Erlang确实使用GC,但是它基本上是以这种方式工作的。
创建多个堆栈?
我们可以假设所有闭包最终都将被调用一次。现在,当调用闭包时,我们可以在闭包返回时进行清理。
我们如何计划如何处理返回的对象?必须在某些时候清理它们,这与关闭完全相同。
我读过ML的最新版本仅少量使用GC
该线程可能会有所帮助,尽管此处的某些答案已经反映了那里的答案。
一张海报很好地说明了这一点:
It seems that you want garbage collection for closures "in the absence of true garbage collection". Note that closures can be used to implement cons cells. So your question seem to be about garbage collection "in the absence of true garbage collection" -- there is rich related literature. Restricting problem to closures does not really change it.
因此,答案是:不,没有优雅的方法可以关闭,也没有真正的GC。
我们能做的最好的事情就是通过一些黑客手段将闭包限制为特定类型的闭包。如果我们拥有合适的GC,那么所有这些都是不必要的。
因此,我的问题在这里反映了其他一些问题,为什么我们不想实现GC?一个简单的mark + sweep或者stop + copy大约需要2-300行(Scheme)代码,并且就编程工作而言并不是那么糟糕。在降低程序速度方面:
- 我们可以实现性能更高的更复杂的GC。
- 试想一下,我们所用语言不会遭受的所有内存泄漏程序。
- 使用GC进行编码是一种祝福。 (考虑一下C#,Java,Python,Perl等……与C ++或者C)。
我知道我已经很晚了,但是我偶然发现了这个问题。
我认为,完全支持闭包确实需要GC,但是在某些特殊情况下,堆栈分配是安全的。确定这些特殊情况需要进行一些转义分析。我建议我们看一下BitC语言文件,例如BitC中的Closure Implementation。 (尽管我怀疑这些文件是否反映了当前的计划。)BitC的设计师也遇到了同样的问题。他们决定为编译器实现一种特殊的非收集模式,该模式拒绝所有可能逸出的闭包。如果打开,它将严重限制语言。但是,该功能尚未实现。
我建议我们使用收集器,这是最优雅的方式。我们还应该考虑到,构建良好的垃圾收集器分配内存的速度比malloc更快。 BitC的人们确实非常重视性能,他们仍然认为,即使对于操作系统的大部分部分Coyotos,GC也可以。我们可以通过以下简单方法减轻不利因素:
- 仅创建少量的垃圾
- 让程序员控制收集器
- 通过逸出分析优化堆栈/堆的使用
- 使用增量或者并发收集器
- 如果可能的话,像Erlang一样分割堆
许多人因为使用Java而害怕垃圾收集器。 Java具有出色的收集器,但是用Java编写的应用程序由于产生大量垃圾而导致性能问题。此外,由于启动和响应时间较长,因此runtime肿的运行时和精美的JIT编译对于桌面应用程序并不是一个好主意。
迟到总比不到好?
我们可能会发现这很有趣:差分执行。
这是一个鲜为人知的控制结构,它的主要用途是对用户界面进行编程,包括在使用过程中可以动态更改的界面。它是"模型-视图-控制器"范例的重要替代方案。
我之所以提到它,是因为人们可能会认为这样的代码将严重依赖于闭包和垃圾回收,但是控制结构的副作用是,至少在UI代码中,它消除了这两个方面。
我猜如果过程很短,这意味着它不能使用太多内存,那么GC就没有必要了。这种情况类似于担心堆栈溢出。不要嵌套得太深,也不会溢出。不要运行太长时间,也不需要GC。清理变得很简单,只需回收我们预先分配的较大区域即可。甚至更长的进程也可以分为较小的进程,这些进程预先分配了自己的堆。例如,这将与事件处理程序一起很好地工作。如果我们正在编写编译器,它将不能很好地工作。在这种情况下,GC肯定不是很大的障碍。