Richard Eisenberg:OCaml 局部值如何减少垃圾回收分配

原标题Richard Eisenberg: How OCaml Locals Reduce Garbage-Collected Allocation

一个带类型注解的列表处理示例说明,创建值和使用值的函数都需要保持局部性。

RERichard Eisenberg讲者 · Jane Street
频道
Jane Street
发布
2024年2月20日
视频
23:25
阅读
6 分钟
幻灯片
5
原文
英语
在 YouTube 打开
往下滚动阅读时,视频会缩到右下角继续播放。字幕跟随阅读语言。
阅读语言共 2 种语言可读
由 AI 从英语原文翻译。打开“对照原文”可以逐段核对。
摘要

30 秒看懂

Richard Eisenberg 使用 Jane Street 的实验性 OCaml 编译器,展示局部值如何减少垃圾回收分配。他构建并测量一个列表处理循环,然后改写迭代器和初始化函数,以表达局部性。 编译器错误揭示了受局部约束的变量与局部分配的数据之间的区别、回调和闭包的作用,以及 exclave 如何让函数在自身区域之外产生局部结果。

  1. 1测量分配示例使用 OCAMLRUNPARAM=v=1024,在程序退出时打印分配和回收统计信息。
  2. 2支持局部值的迭代器列表及其元素不能逃逸,因此迭代器和回调都需要适当的局部性保证。
  3. 3创建位置很重要把变量标记为局部,并不会把 List.init 已创建的列表变成局部分配。
  4. 4通过 exclave 返回自定义初始化函数使用 exclave,使返回的列表在函数自身区域之外分配。
  5. 5解读结果修正调用并将相关值局部化后,示例报告垃圾回收分配了 2,146 个字;局部内存仍会分配,并按区域回收。

要点建议

  1. 局部值不能逃出其区域,因此短生命周期的内存可以在区域结束时回收。
  2. 受局部约束的变量仍然可以持有原本在全局分配的值。
  3. 使用局部列表的函数,也必须对接收局部元素的回调施加适当的局部性约束。
  4. 显式局部性注解可以揭示编译器原本推断出的保证。
  5. 返回局部结果时,exclave 允许表达式在函数自身区域之外分配。
  6. 垃圾回收分配计数下降,并不意味着程序不再分配内存。
全文

按章节阅读整理后的全文

只看原始文稿
01

列表分配实验

Eisenberg 构建可重复的循环,创建列表并统计五的倍数;运行时统计提供垃圾回收分配的基线。

Richard Eisenberg 演示 OCaml 局部值如何减少垃圾回收分配。防止值逃出区域本身就有用,Jane Street 也关注控制短生命周期值的分配位置所带来的性能收益。

演示使用 Jane Street 的实验性编译器分支。视频录制时,团队计划与社区合作,将这些功能合入上游。示例创建许多短列表,并统计其中能被五整除的元素。

函数运行 10,000 次循环。每次迭代随机选择一个小于十的长度,用 List.init 创建由小于 100 的随机整数组成的列表,然后遍历列表,遇到能被五整除的元素时递增一个引用。固定随机种子使结果可以复现。

为了查看分配成本,Eisenberg 查阅 OCaml 运行时文档。将 OCAMLRUNPARAM 设为 v=1024 后,程序退出时会打印垃圾回收统计,包括分配的字数。最初的字节码运行报告约 214,000 个分配字;减少迭代次数后,计数也会下降。

02

让迭代器支持局部值

循环体定义区域,局部值不能逃逸;自定义迭代器和回调表达所需的局部性。

每个列表只在其所属的循环迭代中需要。在手动管理内存的语言中,可以在那里显式释放它;局部值则旨在不手动释放的情况下表达生命周期。每个函数和循环体都定义一个区域,局部值不能逃出其区域。

基线分配测量
幻灯片 1 / 5基线分配测量程序在每次迭代中创建列表,并报告垃圾回收分配统计。

最初把列表标记为局部后,将它传给 List.iter 会产生逃逸错误。在演示使用的编译器和库版本中,标准库函数没有表达所需的局部性。Eisenberg 编写递归的 iter:处理空列表,或对表头应用函数并递归处理表尾。

自定义迭代器能够编译,但分配计数仍然很高。他从最初使用的字节码编译器 ocamlc 切换到为机器生成本地代码的 ocamlopt.opt。计数下降了,但距离他希望达到的接近零垃圾回收分配仍有很大差距。

为了理解迭代器的行为,Eisenberg 写出显式类型注解。没有注解时,编译器可以推断列表不会逃逸;不带 local 注解的类型则不提供这种保证。因此编译器保守地认为列表可能逃逸,并拒绝该调用。

局部迭代器与逃逸错误
幻灯片 2 / 5局部迭代器与逃逸错误编译器指出,传给迭代器的列表缺少所需的局部性保证,该迭代器的显式类型没有表达这种保证。

给列表参数添加 local 注解解决了类型的这一部分。局部列表的元素也不能逃逸,因此回调同样必须接受局部元素;否则它可能把值保存到其他地方。添加这两处注解后,迭代器通过类型检查。

03

创建局部列表

局部变量可以包含全局分配的列表;初始化函数的显式类型和 exclave 解决局部性与尾位置错误。

即便如此,程序仍在分配。局部变量被赋予 List.init 创建的列表,而该函数的结果不保证在局部分配。全局值可以在局部使用:限制变量使其不能逃逸,不会追溯改变值的分配位置。

局部迭代器与 List.init
幻灯片 3 / 5局部迭代器与 List.init列表受到局部约束,但 List.init 仍产生全局分配的值。

为了让列表从创建时就是局部的,Eisenberg 编写替代的 init。递归循环接收索引和累加器,对每个索引应用函数,再将结果用 cons 加到累加器上。循环从长度减一开始,在索引小于零时结束。

实际调用替代函数后,它最初能够编译并产生预期结果 8,987。但没有局部性注解时,它并不能解决分配问题。他添加类型,说明生成元素的函数返回局部值,而 init 返回局部列表。

随后编译器报告某个值逃出了区域。递归循环需要局部累加器,但返回或尾位置又引出了另一个问题。一个非尾调用注解暂时让他更进一步,不过 Eisenberg 希望保留尾调用,因此之后重新处理这个问题。

他为循环给出显式类型,要求它接收并返回局部列表。一个语法错误说明 local_ 与函数箭头相关联:括号放错位置会让注解无法作用于箭头。修正之后,编译器又指出返回处有一次逃逸。

局部初始化函数与返回错误
幻灯片 4 / 5局部初始化函数与返回错误循环的显式类型和编译器提示表明,需要处理结果所属的区域。

每个函数都定义一个区域,但返回的列表应当在函数自身区域之外分配。Eisenberg 添加 exclave,让该表达式提前结束区域,并去掉临时的非尾调用注解。外层 init 也需要 exclave 来返回其局部结果。

这些修改使程序通过编译。局部版本报告约 90,000 个分配字,切换回 List.init 后则约为 177,000。减少的数量有价值,但小于他的预期,因此他进一步检查函数参数和闭包。

04

闭包与最终测量

局部函数参数和局部递归循环进一步减少分配;区分区域分配与垃圾回收计数。

iter 和 init 都接收函数。生成随机值的函数没有捕获环境中的任何内容,因此编译器可以避免为其分配闭包。不过迭代器的回调引用计数器引用,捕获了环境中的状态。

他也将函数参数标记为局部。这暴露了另一个逃逸问题:递归循环捕获了局部函数,因此循环本身也必须是局部的。局部循环不能在 exclave 中用于其区域之外;将 exclave 向外移动解决了这一冲突。

起初测量结果仍没有改善,因为他不小心又调用了 List.init。将调用改为自定义 init 并重新编译后,程序报告的垃圾回收分配很少:屏幕显示分配了 2,146 个字,而结果仍为 8,987。

最终分配测量
幻灯片 5 / 5最终分配测量改用自定义初始化函数后,输出显示垃圾回收分配了 2,146 个字。

关键区别在于,局部值仍然会分配内存。计数器并不是说程序不使用内存,而是表明由自动垃圾回收处理的分配大幅减少。每个循环区域结束时,其局部内存可以作为一组回收,从而减少回收器的工作,并可能改善延迟。

Eisenberg 最后指向示例代码,并表示下一段视频将进一步解释其工作原理以及内存去了哪里。演示中的数字适用于这个小程序以及视频中使用的实验性编译器。

同频道的其他文章

1:34:29EN → 简体中文
JSJane Street· 2026年6月24日Jacob Baskin:构建 Superstore 与调度 HiveYaron Minsky, Jacob Baskin · Jane Street这位 Jane Street 工程师回顾分布式分析数据库背后的设计选择,以及为不断扩大的研究集群分配资源的挑战。基础设施与部署Software engineeringDecision Making+1
1:34:36EN → 简体中文
JSJane Street· 2026年6月1日Nate Foster:把网络当作程序Yaron Minsky, Nate Foster · Jane Street从可编程交换机到 BGP,一位编程语言研究者解释高层策略和形式模型如何让网络更易于修改。基础设施与部署Programming LanguagesSoftware engineering+2
5 张幻灯片15:48EN → 简体中文
JSJane Street· 2026年5月15日走进 Jane Street 的得州 AI 数据中心Dwarkesh Patel, Yaron Minsky, Daniel Pontecorvo · Dwarkesh PatelDwarkesh Patel 与 Ron Minsky、Dan Pontecorvo 一同参观拥有 4,032 个 GPU 的训练设施,了解液冷、配电和运行保障。基础设施与部署AI cost & performance
1:48:21EN → 简体中文
JSJane Street· 2026年3月17日Will Wilson:确定性模拟与软件测试的难题Yaron Minsky, Will Wilson · Jane StreetAntithesis 联合创始人解释可重放模拟如何探索复杂软件、性质为何重要,以及测试的边界在哪里。开发流程与测试Software engineeringCode review & verification+2
7 张幻灯片1:20:57EN → 简体中文
JSJane Street· 2025年12月23日Matt Godbolt:深入 Skylake 的乱序流水线Matt Godbolt以逆向工程和细致测量为基础,导览指令解码、寄存器重命名、执行与内存顺序。SemiconductorsSoftware engineeringBenchmarks
9 张幻灯片55:03EN → 简体中文
JSJane Street· 2025年10月29日Neil Mitchell:Pyrefly 如何大规模检查 Python 代码Neil Mitchell, Roman · Meta从 Python 类型系统到快速开源类型检查器和语言服务器背后的增量架构。Software engineeringOpen-source ecosystems