Skip to content

算法Algorithm

闭包转换

Closure conversion · 闭包变换

把词法捕获改写为显式环境参数,同时保留遮蔽、求值顺序与共享可变单元。

形式陈述 ​

输入、输出与保持的行为 ​

输入是一门从左到右按值求值的小语言:整数、变量、非递归 let、单参数函数、调用、二元组,以及 ref e、!e、e1 := e2。加法按先左后右求值;e1;e2 先丢弃 e1 的结果再计算 e2;fst/snd 投影二元组。三元组 (a,b,c) 是 (a,(b,c)) 的书写糖。按可变存储语义,绑定本身不可变,赋值只改变显式引用单元;两个变量可以保存同一位置。整数按数学整数处理,或要求本例运算落在目标整数范围内。下面没有并发、地址算术、弱引用和终结器。

名字先按绑定身份解析。两个都写成 x 的声明分别得到 x₁、x₂,不会因文本相同而共用槽。输出 IR 只含顶层代码标签、记录分配、字段读写与显式调用;闭包值表示为 Closure(code, env),环境字段保存被捕获的值。若那个值是引用,它保存的是位置,不是该位置里的当前整数。

目标观察是返回的整数或元组、可变单元的读写结果,以及正常终止或发散。增加的环境分配不是源程序可见事件;有限堆上的分配失败与空间代价另列,不能仅凭结果保持就断言资源保持。此处不承诺异常栈、调试变量或函数地址的外观相同。

一套可以执行的转换 ​

编译器维护映射 A,把每个解析后的变量身份映到目标访问表达式。局部变量对应局部临时量;捕获变量对应 env.field。入口 A 将所有外部已解析绑定映到明确的目标参数或全局访问,闭合程序的顶层 A 为空。局部 let x=e1 in e2 先用旧 A 转换 e1 并求值到新鲜临时量 t,再用 A[x↦t] 转换 e2,离开作用域后恢复旧 A。既有自由变量规则沿用,在新构造上增加:非递归 let x=e1 in e2 的捕获集合是 FV(e1) ∪ (FV(e2)−{x});引用创建、读取不引入绑定;写入及二元组取其子表达式集合的并。

转换一个函数 fun y -> e 时,按固定的绑定身份顺序排列 FV(e)−{y},得到 x₁,…,xₖ。建立顶层代码 L(env,y),将这些变量的访问映到 env[1],…,env[k],将形参 y 的身份映到目标形参 y,再递归转换函数体;其他外部普通绑定只有进入捕获字段后才能在此代码体访问。原函数表达式替换为先分配环境 E = Env(A(x₁),…,A(xₖ)),再构造 Closure(L,E)。代码体此后只引用参数、局部量、显式环境和全局代码标签,没有未解释的词法自由变量。

转换应用 e1 e2 时,必须先把转换后的 e1 求值到新临时量 c,再把 e2 求值到新临时量 v,最后执行 call c.code(c.env,v)。不能直接重复展开 e1 来分别取 code 和 env;若 e1 创建引用或修改计数器,重复求值已经改变行为。其余构造逐项保持从左到右顺序。

这是一个语法变换算法,不是运行时每次重新寻找自由变量。若共有 n 个语法节点、K 个实际输出的捕获字段,用哈希集合合并逐节点自由变量集合的直接实现可能需要二次工作;不应笼统写成 O(n)。已给定捕获集合与身份索引后,发出目标代码及字段访问的工作为 O(n+K)。运行时创建捕获 k 项的闭包需 O(k+1) 时间与空间,调用增加常数次字段访问;被捕获对象的整个可达子图不会被复制。

直觉

函数被返回以后,它使用的局部名字去了哪里?闭包给出语义答案:代码带着定义时的环境。闭包转换则给出编译步骤:把环境变成普通记录,把自由变量读取变成字段读取,把函数调用变成“代码地址加环境参数”的调用。

例子与边界

手算:两份环境,一处状态 ​

text
make(seed) =
  let x = ref seed in
  let inc  = fun d -> (x := !x + d; !x) in
  let peek = fun u -> !x in
  (inc, peek)

let p = make(4) in
let a = fst p in
let b = snd p in
let x = 100 in
let first = a(3) in
(first, b(0), x)

inc 的形参是 d,自由变量只有 make 内的 x₁;peek 的形参 u 未被使用,自由变量也是 x₁。最后的 x₂=100 不在任何捕获集合里。为使布局直观,本例为两个函数分别建立一字段环境;它们可以共享环境记录,但没有必要这样优化。

text
inc_code(E, d):
  C = E.cell
  old = load C.value
  store C.value = old + d
  return load C.value

peek_code(E, u):
  return load E.cell.value

make_code(E_unused, seed):
  C  = alloc Cell(seed)
  Ei = alloc Env(C)
  A  = alloc Closure(inc_code, Ei)
  Ep = alloc Env(C)
  B  = alloc Closure(peek_code, Ep)
  return alloc Pair(A, B)

make(4) 返回时,C.value=4,Ei.cell=Ep.cell=C。调用 A.code(A.env,3) 把同一个 C.value 改为 7;再调用 B.code(B.env,0) 沿 B→Ep→C 读到 7。外层调用点的 x₂ 仍为 100,所以结果是 (7,7,100)。make 的调用帧已经退出,但这些堆记录的生命期没有随它结束。

不同闭包可共享同一个可变位置

一个真正的错误变体是把 Env(C) 改成 Env(load C.value),再让每个函数更新各自环境中的数值。此时 inc(3) 返回 7,peek(0) 仍返回 4,结果变为 (7,4,100)。这不是无害的布局优化,而是把一处共享状态拆成了两处独立状态。

递归初始化是额外一步 ​

若扩展到 let rec f = fun y -> e in body,函数体里对 f 的调用需要找到同一闭包。一个明确的实现是:先创建包含 self=null 的环境,再创建闭包 F=Closure(L,E),最后写入 E.self=F,函数体把 f 读作 E.self。环境与闭包形成一个允许自引用的循环。

分配期间若可能收集,尚未完成的 E、F 必须保存在已登记的根槽;未初始化指针字段必须先是合法空值。最后写入 self 的短序列不得在无有效根的状态触发收集。此规则只覆盖“递归右侧是函数”的受限 let rec;任意递归值需要另定初始化或拒绝规则。本页主例不依赖递归扩展。

推论与应用

为什么转换保持结果 ​

建立源位置到目标 Cell 的一一对应 h。源整数与同值目标整数对应;源引用 ℓ 与 h(ℓ) 对应;源闭包与一个目标闭包对应,当且仅当目标代码是该函数的转换,并且每个捕获字段对应源定义环境中的同一绑定值。特别地,若两个源引用相等,它们都映到同一个 h(ℓ)。

对求值推导归纳:常量与局部读取直接保持对应;创建引用扩展 h;读写通过同一个 h(ℓ) 保持存储关系;函数创建按捕获集合建立闭包关系;调用先对应地计算函数与实参,再用环境字段和形参进入对应函数体。因此每次写后,所有指向同一单元的别名仍观察到相同值。元组按字段逐项对应即可。

这是对本页终止求值的证明结构。若要同时得到发散保持,须把构造过程表为逐步模拟,证明有限管理步骤不会自行引入无限循环;若要得到空间界,还须分析环境保留了哪些对象。共享一个含无用大对象的外层环境,可能延长大对象的可达生命期。文献中的 safe-for-space 定理有自己的语言、成本语义和转换,不能直接当作本页任何环境优化的保证。[2]

迁移练习 ​

把 make 改成返回 inc、peek、reset 三个闭包,reset(v) 写 x:=v。执行 inc(3); reset(10); peek(0) 应得到 10;三个环境必须指向同一个单元。若另调用一次 make(4),第二次分配得到新单元,两个计数器组不能互相影响。验收时分别写出“组内别名相同”和“组间位置不同”,这比只比较一次输出更能检查转换。

为了检验运行时收集,可以在 inc 写入后插入一个源语言不可见的 gc_poll。本页这段 IR 尚未插点;根映射页给出插点后的完整片段,明确丢弃旧 C 并在返回前从已更新环境重新取 C,不能把本页未重载的片段直接接到移动收集器上。

接下来,调用约定与栈帧决定显式环境参数放在哪里;逃逸分析解释哪些对象不能随帧释放;复制收集则检验搬家以后这些别名是否仍相同。

Lambda 提升对已知直接调用的局部函数增加普通参数,可以不为这些调用创建环境记录;传递需求还须沿调用图闭合。返回函数值不能只返回提升后的代码地址,因为未来调用者未必持有捕获值,所以本页的返回闭包与共享单元仍由显式环境表示承担。前端可先用声明身份解析固定捕获对象,再经ANF把有副作用的求值次序显式化。

参考资料

[1] Andrew Myers,Cornell CS 4120/5120,Compiling first-class functions,2021,§2 “Closures and closure conversion”、§§3–4 “Escaping variables / Static link chains”。用于核对闭包实现接口;本页程序与转换轨迹为独立教学例。

[2] Zoe Paraskevopoulou 与 Andrew W. Appel,Closure Conversion Is Safe for Space,ICFP 2019,作者机构摘要限定的 flat-environment CPS 转换。该文的资源定理未在本页重证,也未外推到任意环境共享方式。

关系图谱13 个相邻概念 · 3 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系