【问题标题】:Garbage collection issue in an interpreter implemented in C用 C 实现的解释器中的垃圾收集问题
【发布时间】:2013-03-29 21:01:47
【问题描述】:

我正在为一种玩具过程语言开发一个爱好编译器/解释器,并且我已经实现了我打算探索的大部分功能,除了一个好的垃圾收集算法(类似于this guy)。我已经阅读了很多关于各种算法的内容,并且对如何实现它们有了大致的了解。我的语言运行时的早期迭代使用了引用计数,但我放弃了它以学习更高级的东西,所以我现在正在考虑使用标记和复制压缩算法。

我开始的第一个问题是阻止算法在本机扩展函数(即用 C 编写的函数)中收集“对象”。根集由解释器堆栈上的“对象”和符号表中的“对象”组成,我不应该对这些有太多麻烦,但是,如果在 C 函数中创建容器“对象”,然后填充子“对象”,我如何防止 GC 收集它们,因为它实际上不在解释器堆栈上或绑定到符号?

使实施 GC 更容易的事情:

  • 我的语言中的所有“对象”都是内置类型(例如,不是面向对象的)
  • 解释器堆栈只是一个指向结构的指针堆栈
  • 符号表只是指向结构的指针数组

用户代码:

f = open('words.txt', 'r');
lines = readlines(f);
close(f);

解释器(解析后,编译成字节码...):

  1. push 文件名,打开模式
  2. 调用builtin_fopen,它返回一个包装FILE*的结构
  3. 将结果存储在符号f
  4. 推符号f
  5. 调用builtin_flines 创建一个列表类型l,然后使用C fread 读取每一行 将文件作为字符串类型,将其附加到列表l
  6. 将结果存储在符号lines 中,以此类推....

现在,如果 GC 在分配文件中包含一行的字符串之一时运行,则根集还没有对 l 的任何引用,因此它应该被收集。 有关如何更好地处理此问题的任何想法?

【问题讨论】:

  • "mark and copy" -- 大概你的意思是mark and sweep。解释器分配的所有对象都应该在它们自己的堆中,与扩展函数使用的对象分开。如果你想 GC 扩展函数内存,你正在寻找类似 Boehm 保守 GC 的东西。
  • 我的意思是 mark and compact 复制可访问对象。这个想法是扩展函数分配的对象旨在被放入解释器堆栈和/或绑定到符号,因此应该在以后进行垃圾收集。
  • 你可能会发现我感兴趣的问题here
  • 我认为我的解决方案是 Chris Dodd,n.m. 和 Derek Pressnall 的答案的结合。 n.m. 将特殊分配保持在堆顶指针顶部上方的方法很有用,但不能解决本机函数执行过程中的垃圾收集问题。 Chris Dodd 为本地函数创建运行时接口以获取对象句柄的方法很棒,但 Derek Pressnall 实际上解释了如何做到这一点,方法是将对象指针的引用传递给 mutator。这个解释得到我的50分。

标签: c garbage-collection interpreter


【解决方案1】:
  1. 为解释器的堆指定一个单独的连续分配区域。切勿在竞技场之外收集任何东西。
  2. 您始终拥有竞技场的当前顶部(假设它从较低地址增长到较高地址)。顶部以上的所有内容均不可收藏,但在根集中考虑。必须分配多个链接对象的内置函数将它们分配到顶部上方,然后将顶部向上移动,以便所有分配的对象立即最终进入可回收堆。如果收集发生在函数执行过程中,则顶部上方的对象将立即全部移至新堆。

【讨论】:

  • 这听起来是个不错的方法,但是如果收集发生在本机函数执行过程中,我不确定如何处理更新引用指针。
【解决方案2】:

您需要为您的本机函数提供一个接口,通过该接口它们可以告诉垃圾收集器它们引用了哪些对象,然后让它们使用该接口。

最简单的方法可能是根本不让本机代码直接指向解释器/垃圾收集的数据。相反,您为本机代码提供对象的句柄并让它回调到运行时以从对象中获取值。在您的示例中,builtin_flines 将调用运行时分配一个列表并取回它的句柄。然后它会读取行,并调用运行时将每一行附加到列表中,最后返回完整的列表。运行时将管理给定本机调用的所有句柄,并在本机调用返回后释放它们。

【讨论】:

  • 我希望本机函数使用相同的 API 来分配新对象,例如LuciObject *list = LuciList_new(),在解释器中使用。将一种类型的堆栈传递给所有本机函数并要求它们立即将分配的对象压入堆栈是不合理的吗?解释器/垃圾收集器可以访问堆栈,以便将其包含在根集中。
【解决方案3】:

因为我是你提到的最初的“这个”人,但我可以根据我迄今为止在项目中设计的内容为你提供关于你的第一个问题的一些见解(我保证我最终会在博客上介绍它)。因此,首先,所有内存分配都通过一个 mutator 函数。输入参数是您正在创建的对象的类型,以及对指向它的指针类型对象的引用。然后在创建新对象时更新该指针对象。如果一个对象被分配给解释器运行时的 C 函数独占使用,那么它就是一个根对象。在这种情况下,NULL 作为第二个参数传递,并且该对象被添加到根对象列表中。现在稍后,如果该内部函数不再需要该对象,则必须从根对象列表中删除该对象。 (它不会取消分配对象本身,因为最终将由垃圾收集例程处理)。哦,解释器堆栈本身也是解释器中的一个对象(列表类型或数组类型对象),因此指向它的指针也在根对象列表中(同样,另一个列表类型对象也是口译员知道)。指向根对象列表的指针是垃圾收集器需要知道的唯一指针。

另外,关于何时开始运行垃圾收集器——因为现代架构上的内存实际上是无限的,所以我决定在分配 X 个对象时启动垃圾收集器。运行后,您还剩下 Y 个对象。如果 Y 仍然大于 X 的 Z 百分比,那么 X 会被抬高到足以做到这一点。然后我只希望 malloc() 永远不会失败(如果失败了,我只是转储一个错误并退出解释器)。

希望这会有所帮助,并希望其他人会添加更多说明,因为我在语言/解释器设计方面更像是一个业余爱好者。

【讨论】:

  • 基本上,Object *tmp = alloc(obj_type, &tmp)。这允许收集器存储tmp 的地址并在任何给定时间更改其值。老实说,我从来没有玩过这样的指针,但它很好。
【解决方案4】:

一些并发症:

当您输入要解释为的行时 100 如果 X 然后 gosub 5000

但是 5000 还不存在,你是意大利面条编码... 也许 x 还没有任何赋值或数据类型。 如果我们现在不索引,我们要等到有人 键入“运行”还是直接从提示符执行一行?

如果我们现在做索引以加快速度,我们将如何 知道“100”或“X”或“5000”的最后一个实例得到 删除?

我们在“事物”的主索引中创建了什么条目? 假设这些东西可能包括基本代码行, 字符串,以及我们想要按名称处理的其他变量 或行号。

我们希望快速找到,并用于战略性地识别 垃圾收集潜在的时候需要收集 出现。

我们在事物的索引上消耗了多少静态空间 这可能会改变大小?除了标签还有哪些细节, 位置和长度足以证明索引的合理性? 我们是否应该尝试在变量时索引空白空间 缩小?或者只是索引变量的最大历史 大小连同它当前的大小?我们如何识别那些 大小变化最频繁的变量,应该 我们避免清洁它们,甚至故意垫它们?

我们什么时候清理整个烂摊子?还是更好 碎片整理只有足够的可用空间来挤入 不能以其他方式从侧面卡入现有的孔中?

有目的的延迟和等待“输入”似乎是不错的目标 我们可能会利用它来主动清理一些 混乱。不保证任何基本程序都会有这样的 死区时间。

对不起,这不是答案,但原来的问题似乎 邀请一些头脑风暴来制定更好的计划。我们需要 一个明确的策略,需要定义整个问题。

【讨论】:

  • 抱歉,SO 并不是一个用于头脑风暴的网站,正如您所说的那样。这不是论坛。这是一个问答网站。我知道您没有足够的声誉在任何地方发布 cmets,但这仍然不是您应该在 SO 上发布什么样的答案的好例子。我不会对答案投反对票,因为您是 SO 新手。
猜你喜欢
  • 1970-01-01
  • 2015-04-09
  • 1970-01-01
  • 2011-06-01
  • 1970-01-01
  • 2019-02-01
  • 2011-10-15
  • 1970-01-01
  • 2013-04-01
相关资源
最近更新 更多