30 秒看懂
在这期 OCaml Unboxed 中,讲者比较了堆分配和垃圾回收与独立 local 栈上的分配。循环例子展示了不逃逸的值如何在区域结束时回收,以及 local 值为什么可能需要在函数返回后继续存在。
- 1反复进行堆分配每次迭代都会构建新列表,引用改变后,之前的列表变得不可达。
- 2回收的成本简化的标记清除例子展示了回收堆对象所需的工作和可能的暂停。
- 3区域限定生命周期在循环体内分配的 local 值不能逃出该区域。
- 4为什么需要独立栈exclave 可以让值在函数返回后继续存在,同时仍限制在外围区域内。
要点建议
- 不使用 local 时,例子会反复在堆上分配列表。
- 垃圾回收会回收不可达的值,但可能增加工作量和延迟。
- 循环体形成一个区域,其中的 local 值不能逃逸。
- 区域结束后,可以一起回收并复用其中的 local 分配。
- local 栈与调用栈分开,因为一些 local 值的寿命长于分配它们的函数。
按章节阅读整理后的全文
列表分配示例
讲者重新审视一个通过 local 注解减少垃圾回收分配的程序。例子反复构建和处理列表。
在这期 OCaml Unboxed 中,我继续探索我的 Jane Street 同事和我正在开源编译器分支中开发的 local 模式。之前的例子通过 local 注解减少了需要垃圾回收的分配。今天我想展示内存去了哪里,以及这种做法为什么有效。
例子的主函数 doit 使用一个迭代变量和一个列表进行循环。它调用 init 构建列表,然后计算其中 5 的倍数的个数。首先,假设程序没有 local 注解,观察堆上的分配。
堆分配与垃圾回收
不使用 local 时,旧列表变得不可达,随后由垃圾回收机制回收。讲者用简化的标记清除模型解释其成本。
init 在堆上构建由 cons 单元组成的链表,list 变量指向列表开头。下一次迭代中,init 构建另一个列表,list 改为指向新列表。旧单元无法再通过该变量访问。
垃圾回收运行时,简化后的过程是从栈变量等根开始追踪,标记可达的堆对象,并回收不可达对象。当前列表仍然存活;旧列表可以被回收,为后续分配腾出空间。

垃圾回收很有价值:它支持不可变的函数式编程,使程序更容易推理和开发。但寻找存活数据并回收其余数据需要工作。在标记清除模型中,回收可能暂停程序执行,并访问可能挤掉更有用缓存数据的内存。
Jane Street 的大多数程序使用垃圾回收都没有问题。但对于延迟敏感的场景,我们希望避免这些暂停和其他减速。这一性能目标促使我们研究 local。
独立的 local 栈
local 分配被限制在区域内,可以在区域结束时一起回收。独立的栈允许值在函数返回后继续存在,又不逃出其区域。
现在来看使用 local 的版本。调用栈仍然保存 doit 及其变量,但 init 在一个独立区域中分配列表单元,我称之为 local 栈。它既不是普通调用栈,也不是由垃圾回收管理的堆。
init 完成并返回局部分配的列表,doit 可以用它计算 5 的倍数的个数。循环体是一个区域:在该区域内分配的 local 值不能逃出去。迭代结束时,分析保证这些分配不再需要。

运行时可以一起回收该区域的 local 分配,并在下一次迭代中复用同一块内存。释放操作可以简单到只需改变下一次分配所用的指针,从而避免堆示例中描述的标记清除工作。

独立的 local 栈很重要,因为 init 可以在调用者用完列表之前返回。在 exclave 内创建的值可以在函数返回后继续存在,同时仍限制在外围区域内。函数栈帧和区域的生命周期并不总是相同,因此这些 local 分配不能简单地放在 init 的调用栈帧中。
同频道的其他文章
15:33EN → 简体中文
1:48:29EN → 简体中文
1:36:37EN → 简体中文
1:05:45EN → 简体中文
1:25:00EN → 简体中文
1:16:29EN → 简体中文