30 秒看懂
Chris Casinghino 解释了 OCaml 统一表示和多核内存模型的权衡,随后介绍 Jane Street 在未装箱布局、栈分配和无数据竞争保障方面的工作。2025 年演讲时,未装箱类型和栈分配已用于生产,而无数据竞争支持正开始推广。
- 1统一的值表示经典 OCaml 用一个字表示值,该值可以是立即数或指向堆块的指针。
- 2统一表示的代价一个字节可能占用一个机器字,装箱的 int64 需要堆块,影响局部性和垃圾回收。
- 3未装箱布局种类描述类型的表示方式,使基本类型和记录更紧凑,同时让编译器了解其布局。
- 4局部与全局模式模式追踪值是否能逃逸作用域,从而支持安全的栈分配和资源 API。
- 5通过模式防止数据竞争争用模式追踪共享情况,可移植性模式控制值是否能在线程间传递。
- 6生产使用与推广Casinghino 表示,栈分配和未装箱类型已用于生产,无数据竞争功能正开始测试推广。
要点建议
- 统一的单字值表示简化了垃圾回收和多态,却限制了对内存布局的控制。
- 未装箱类型和布局种类让编译器能够追踪更窄、更扁平的表示。
- 局部模式可以强制遵守栈纪律,防止值逃逸其作用域。
- 争用与可移植性模式有助于静态防止不安全的跨线程访问。
- 设计目标包括保留类型和内存安全、类型推断、兼容性,并让复杂度随使用需求增加。
- 据 2025 年的介绍,部分功能已用于生产,无数据竞争保障则处于推广初期。
按章节阅读整理后的全文
OCaml 值、多态与并行性
Casinghino 解释统一的值表示、类型推断和多核 OCaml,并对比 OCaml 对数据竞争影响的限制与静态无竞争保障。
我在 Jane Street 的 OCaml 语言团队工作,该团队属于工具与编译器组。我们构建库和开发工具,也开发语言扩展。今天我会讨论 OCaml 在高性能软件方面可以改进的地方,以及我们为解决这些问题而构建的功能。
我会先解释 OCaml 的一些现有行为,因为理解它们的收益和成本对新设计很重要。第一个细节是内存中值的统一表示。
在经典表示中,每个 OCaml 值占一个机器字。它要么是将数据打包在字中的立即数,要么是指向堆块的指针。在 64 位机器上,立即数有 63 位数据和一位标签;整数、字符和布尔值使用这种形式。

不能放进这种表示的值通过指向堆块的指针表示,堆块包含头部和字段。统一表示帮助垃圾回收器区分指针与立即数,并扫描相关的堆数据。
OCaml 还具有表达力丰富的静态类型、类型推断和广泛使用的多态。例如,map 函数将一个函数应用到列表上,产生另一个类型的列表。编译器可以推断它的泛型类型,无需程序员编写注解。
统一的值使多态函数的单独编译很直接:一个编译后的函数可以处理使用相同单字调用约定表示的类型。相比之下,C++ 泛型通常为各类型创建特化副本,以代码体积和指令缓存压力换取优化机会。
其他语言实现可以通过即时编译在运行时生成特化代码。接下来 Casinghino 谈到 OCaml 5:它引入了共享内存的多核并行,要求运行时和垃圾回收器能够处理并发访问。
他认为 OCaml 的内存模型将数据竞争的影响限制在一定的空间和时间范围内。相比之下,C++ 将数据竞争视为未定义行为,后果可能出人意料且难以定位。数据竞争涉及对同一内存的并发访问,且至少有一次写入。
这些限制让调试更容易,但 OCaml 5 本身没有提供静态的无竞争保障。安全 Rust 通过类型系统提供这种保障。Casinghino 希望在不失去 OCaml 熟悉特性的情况下,给它带来类似能力。
性能目标与未装箱布局
统一表示带来空间和局部性成本。团队在保持兼容性和安全性的同时,引入布局种类和未装箱类型。
统一表示也限制了对内存布局的控制。8 位数据可能占用整个机器字。装箱的 int64 无法放进立即数的 63 位;在所描述的例子中,它需要一个包含元数据的三字堆块。

指针数组可能将数据分散在堆中,损害预取和缓存局部性,而回收器必须管理这些分配的块。OCaml 的回收器很快,但减少不必要的分配仍有助于性能敏感的程序。
即使内存模型限制了影响范围,数据竞争仍是错误。运行时同步也有成本。静态保障可以防止竞争,并可能避免部分检查,同时保持语言安全。
设计目标是安全、方便且可预测地控制性能关键行为。在保留类型与内存安全及 OCaml 高层风格的同时,让程序员表达所需行为,而不是完全依赖可能因微小代码修改而变化的优化启发式规则。
大多数开发者不需要逐纳秒调优,因此只有使用相关功能时才应承担额外复杂度。团队还希望兼容上游 OCaml 和公共库,并希望将这些功能贡献回上游。
三个核心功能是窄而扁平的数据布局、栈分配和无数据竞争的并行编程。团队使用类型系统中称为种类(kind)和模式(mode)的概念表达所需属性。
int32#、int8# 和 float# 等未装箱基本类型可以使用与底层位数相符的表示,而不是经典的装箱值或带标签机器字。它们可以放入记录,使字段表示更加紧凑。

未装箱记录向程序员呈现记录抽象,而其组成部分可以分别传递,例如通过寄存器传递,而不是通过堆指针。未装箱记录数组可以让字段在内存中相邻排列,而不是存储一个指针数组。
这带来一个问题:垃圾回收器和多态代码此前依赖统一的值。拟议的类型系统追踪称为布局的表示信息,使编译器和回收器知道如何处理不同形状的数据。
种类可以看作对类型的分类,正如类型对值进行分类一样。布局包括经典的 value 表示、位宽、浮点布局,以及用于不需要运行时表示的值的 void。

any 布局表示具体布局未知的类型。没有更多信息时,使用它的代码不能直接编译,但它可用于共享签名,具体实现再特化到确定的布局。聚合布局用于描述字段组合。
对于多态函数,编译器可以按布局而不是按每个单独类型生成副本。回收器也需要新的元数据:混合记录的头部可以标识哪些字段必须扫描,以及哪些未装箱字段不是指针。
使用模式实现安全栈分配
栈分配可以减轻垃圾回收压力,但要求值不能逃逸。局部和全局模式表达这一约束,也有助于资源管理。
第二个功能是栈分配。OCaml 的堆分配效率很高,尤其是次要堆,但回收和缓存局部性仍有成本。更大的次要堆需要回收的频率较低,却可能无法很好地装入缓存。
栈分配同样会移动指针,但函数返回时只需将栈指针移回,就能回收空间,无需逐个扫描值。复用栈空间可以触及更少的缓存行。难点是确保引用绝不会比所分配的值活得更久。

C 中经典的释放后使用错误,是存下一个指向栈上值的指针,然后在函数返回后使用它。安全的栈分配需要遵守栈纪律:不要在堆中创建指向局部值的引用,也不要在其生命周期结束后返回它。
为什么不直接使用 Rust 风格的生命周期?Casinghino 说,完整追踪任意生命周期可能需要注解和复杂的高阶推理,这与方便推断的目标冲突。OCaml 可以改为把生命周期复杂的值放在垃圾回收堆中。
模式提供与类型正交的属性。布局描述表示方式,而模式追踪值可以如何使用,包括嵌套值的深层属性。第一个区分是 global 和 local。
global 是传统默认模式,不限制生命周期。local 表示值遵守栈纪律,不能通过存入堆或在作用域结束后返回而逃逸。类型系统追踪使用情况,以判断这一保障是否成立。
这里存在子模式关系:global 值可以安全地用于期望 local 值的地方,因为这样做只会对其使用施加更多限制。这让已有的长生命周期值也能参与要求局部行为的函数。

以 map 为例:它调用函数参数,却不存储或返回该函数。将函数参数标为 local 就能表达这一点。multiply_by 这样的调用方随后可以在栈上而不是堆上为乘数创建闭包,避免一次垃圾回收器分配。

局部性除了支持栈分配,还有助于保证正确性。with_file 函数打开文件,将通道传给所提供的函数,之后关闭文件。如果回调保存通道供以后使用,就可能暴露已关闭的句柄;local 约束可以阻止通道逃逸。
无数据竞争保障与推广
争用和可移植性模式支持安全的线程 API 与共享内存。Casinghino 最后介绍生产使用和初步推广情况。
模式还可以通过追踪值的操作来支持无数据竞争保障,包括别名和跨线程共享。第一个新增维度是争用:uncontended 值局限于一个线程,contended 值则已经共享。

类型系统可以禁止读取或写入 contended 值的可变部分。这阻止了一种明显的数据竞争来源,但实用系统还必须提供受控方式,安全地修改共享状态。
可移植性关注另一个问题:值是否可以发送给其他线程。捕获了自身能修改的可变状态的函数不可移植,因为发送该闭包也会传递修改另一个线程状态的能力。

系统有多个模式维度。Casinghino 区分已经发生的事情,例如值是否已共享或已有别名,以及以后可能发生的事情,例如值是否可以逃逸或共享。他还提到唯一性和线性等维度。
线程创建 API 可以要求传入可移植函数,从而防止不安全的闭包情况。join 操作等待另一个线程结束并返回其值;由于那个线程已不再运行,返回数据不必仅因为跨越线程边界就继续保持 contended 状态。
为安全使用可变共享内存,他勾勒了一个基于锁的 API。类型参数将共享单元与特定的锁关联起来,操作时需要匹配的键。键不能发送给其他线程,因此持有键有助于在操作受保护数据时实现独占访问。

2025 年演讲时,栈分配和未装箱类型已经用于生产,但仍有更多工作要做。根据 Casinghino 的介绍,无数据竞争功能的实现已经完成,但正处于推广初期,由部分团队测试 API。
他表示,这些功能帮助重视性能的开发者用更少的变通方法编写更符合 OCaml 风格的代码。团队也在为新模式给库添加注解,并关注对不需要这些功能的用户而言,额外复杂度是否仍然可控。
同频道的其他文章
1:48:29EN → 简体中文
1:36:37EN → 简体中文
1:05:45EN → 简体中文
1:25:00EN → 简体中文
1:16:29EN → 简体中文
27:39EN → 简体中文