Lean 语言参考

12.2. 引用计数🔗

Lean 使用 引用计数进行内存管理。 每个分配的对象都会维护有多少其他对象引用它的计数。 添加新引用时,计数会增加;删除引用时,计数会递减。 当引用计数达到零时,该对象将不再可访问,并且无法参与程序的进一步执行。 它被释放,并且它对其他对象的所有引用都被删除,这可能会触发进一步的释放。

引用计数提供了许多好处:

内存的重用

如果一个对象的引用计数在分配另一个相同大小的对象时降至零,则原始对象的内存可以安全地重新用于新对象。 因此,当要遍历的数据结构只有一个引用时,许多常见的数据结构遍历(例如 List.map)不需要分配内存。

机会性就地更新

基本类型(例如 stringsarrays)可以提供复制共享数据但就地修改非共享数据的操作。 只要它们保留对正在修改的值的唯一引用,对这些基本类型的许多操作都会修改它而不是复制它。 这可以带来显着的性能优势。 精心编写的 Array 代码避免了不可变数据结构的性能开销,同时保持纯函数提供的推理简易性。

可预测性

引用计数会在可预测的时间递减。 因此,引用计数对象可用于管理其他资源,例如文件句柄。 在 Lean 中,Handle 不需要显式关闭,因为当它不再可访问时会立即关闭。

更简单 FFI

作为回收未使用内存的一部分,不需要重新定位使用引用计数管理的对象。 这极大地简化了与用其他语言(例如 C)编写的代码的交互。

引用计数的传统缺点包括由于更新引用计数而导致的性能开销以及无法识别和释放循环数据。 通过基于“借用”的分析可以最小化前一个缺点,该分析允许省略许多引用计数更新。 然而,多线程代码需要在线程之间同步引用计数更新,这也会带来很大的开销。 为了减少这种开销,Lean 值被分为可从多个线程访问的值和不可访问的值。 单线程引用计数的更新速度比多线程引用计数快得多,并且许多值只能在单个线程上访问。 这些技术共同大大降低了引用计数的性能开销。 由于 Lean 的可验证片段无法创建循环数据,因此 Lean 运行时没有检测它的技术。 Ullrich and de Moura (2019)Sebastian Ullrich and Leonardo de Moura, 2019. “Counting Immutable Beans: Reference Counting Optimized for Purely Functional Programming”. In Proceedings of the 31st Symposium on Implementation and Application of Functional Languages (IFL 2019). 提供有关引用计数在 Lean 中实现的更多详细信息。

12.2.1. 观察独特性🔗

确保数组和字符串被唯一引用是在 Lean 中编写快速代码的关键。 原语 dbgTraceIfShared 可用于检查数据结构是否存在别名。 调用时,它返回其参数不变,如果参数的引用计数大于 1,则打印提供的跟踪消息。

🔗def
dbgTraceIfShared.{u} {α : Type u} (s : String) (a : α) : α
dbgTraceIfShared.{u} {α : Type u} (s : String) (a : α) : α

Display the given message if a is shared, that is, RC(a) > 1

由于 Lean.Parser.Command.eval : command`#eval e` evaluates the expression `e` by compiling and evaluating it. * The command attempts to use `ToExpr`, `Repr`, or `ToString` instances to print the result. * If `e` is a monadic value of type `m ty`, then the command tries to adapt the monad `m` to one of the monads that `#eval` supports, which include `IO`, `CoreM`, `MetaM`, `TermElabM`, and `CommandElabM`. Users can define `MonadEval` instances to extend the list of supported monads. The `#eval` command gracefully degrades in capability depending on what is imported. Importing the `Lean.Elab.Command` module provides full capabilities. Due to unsoundness, `#eval` refuses to evaluate expressions that depend on `sorry`, even indirectly, since the presence of `sorry` can lead to runtime instability and crashes. This check can be overridden with the `#eval! e` command. Options: * If `eval.pp` is true (default: true) then tries to use `ToExpr` instances to make use of the usual pretty printer. Otherwise, only tries using `Repr` and `ToString` instances. * If `eval.type` is true (default: false) then pretty prints the type of the evaluated value. * If `eval.derive.repr` is true (default: true) then attempts to auto-derive a `Repr` instance when there is no other way to print the result. See also: `#reduce e` for evaluation by term reduction. #eval 实现方式的具体情况,将 dbgTraceIfSharedLean.Parser.Command.eval : command`#eval e` evaluates the expression `e` by compiling and evaluating it. * The command attempts to use `ToExpr`, `Repr`, or `ToString` instances to print the result. * If `e` is a monadic value of type `m ty`, then the command tries to adapt the monad `m` to one of the monads that `#eval` supports, which include `IO`, `CoreM`, `MetaM`, `TermElabM`, and `CommandElabM`. Users can define `MonadEval` instances to extend the list of supported monads. The `#eval` command gracefully degrades in capability depending on what is imported. Importing the `Lean.Elab.Command` module provides full capabilities. Due to unsoundness, `#eval` refuses to evaluate expressions that depend on `sorry`, even indirectly, since the presence of `sorry` can lead to runtime instability and crashes. This check can be overridden with the `#eval! e` command. Options: * If `eval.pp` is true (default: true) then tries to use `ToExpr` instances to make use of the usual pretty printer. Otherwise, only tries using `Repr` and `ToString` instances. * If `eval.type` is true (default: false) then pretty prints the type of the evaluated value. * If `eval.derive.repr` is true (default: true) then attempts to auto-derive a `Repr` instance when there is no other way to print the result. See also: `#reduce e` for evaluation by term reduction. #eval 一起使用可能会产生误导。 相反,它应该在显式编译和运行的代码中使用。

Observing Uniqueness

该程序读取用户输入的一行,并在用空格替换其第一个字符后打印它。 如果字符串不共享并且字符都包含在 Unicode 的 7 位 ASCII 子集中,则替换字符串中的字符将使用就地更新。 dbgTraceIfShared 调用不执行任何操作,表明该字符串确实会就地更新而不是复制。

def process (str : String) (h : str.startPos str.endPos) : IO Unit := do IO.println ((dbgTraceIfShared "String update" str).startPos.set ' ' h) def main : IO Unit := do let line := ( ( IO.getStdin).getLine).trimAscii.copy if h : line.startPos line.endPos then process line h

使用此输入运行时:

stdinHere is input.

程序发出:

stdout ere is input.

具有空的 标准错误 输出:

stderr<empty>

该版本的程序保留了对原始字符串的引用,这需要将调用中的字符串复制到 String.set。 这一事实在其 标准错误 输出中可见。

def process (str : String) (h : str.startPos str.endPos) : IO Unit := do IO.println ((dbgTraceIfShared "String update" str).startPos.set ' ' h) def main : IO Unit := do let line := ( ( IO.getStdin).getLine).trimAscii.copy if h : line.startPos line.endPos then process line h IO.println "Original input:" IO.println line

使用此输入运行时:

stdinHere is input.

程序发出:

stdout ere is input.Original input:Here is input.

在其 标准错误 中,传递给 dbgTraceIfShared 的消息是可见的。

stderrshared RC String update

12.2.2. 编译器IR🔗

编译器选项 trace.compiler.ir.result 可用于检查函数的编译器中间表示 (IR)。 在此中间表示中,引用计数、分配和重用是明确的:

  • isShared 运算符检查引用计数是否为 1

  • ctor_n 分配类型的第 n 个构造函数。

  • proj_n 从构造函数值中检索 nth 字段。

  • set x[n] 改变 x 中构造函数的 nth 字段。

  • ret x 返回 x 中的值。

引用计数操作的细节可能取决于优化过程(例如内联)的结果。 虽然绝大多数 Lean 代码不需要这种关注来实现良好的性能,但在编写性能关键型代码时,了解如何诊断独特的引用问题可能非常重要。

🔗option
trace.compiler.ir.result

Default value: false

enable/disable tracing for the given module and submodules

Reference Counts in IR

编译器 IR 可用于观察引用计数何时递增,这有助于诊断预期值具有唯一传入引用但实际上是共享的情况。 这里,processprocess'各自以一个字符串作为参数,并用String.set修改它,返回一对字符串。 process 返回常量字符串作为该对的第二个元素,而 process' 返回原始字符串。

set_option trace.compiler.ir.result true def [Compiler.IR] [result] def process._closed_0 : obj := let x_1 : obj := ""; ret x_1 def process (x_1 : obj) : obj := let x_2 : tagged := 0; let x_3 : u32 := 32; let x_4 : obj := String.set x_1 x_2 x_3; let x_5 : obj := process._closed_0; inc x_5; let x_6 : obj := ctor_0[Prod.mk] x_4 x_5; ret x_6process (str : String) : String × String := (str.`String.set` has been deprecated: Use `String.Pos.Raw.set` instead Note: The updated constant is in a different namespace. Dot notation may need to be changed (e.g., from `x.set` to `String.Pos.Raw.set x`).set 0 ' ', "") def [Compiler.IR] [result] def process' (x_1 : obj) : obj := let x_2 : tagged := 0; let x_3 : u32 := 32; inc x_1; let x_4 : obj := String.set x_1 x_2 x_3; let x_5 : obj := ctor_0[Prod.mk] x_4 x_1; ret x_5process' (str : String) : String × String:= (str.`String.set` has been deprecated: Use `String.Pos.Raw.set` instead Note: The updated constant is in a different namespace. Dot notation may need to be changed (e.g., from `x.set` to `String.Pos.Raw.set x`).set 0 ' ', str)

process 的 IR 不包含 incdec 指令。 如果传入的字符串 x_1 是唯一引用,那么当传递给 String.set 时它仍然是唯一引用,然后可以使用就地修改:

[Compiler.IR] [result]
    def process._closed_0 : obj :=
      let x_1 : obj := "";
      ret x_1
    def process (x_1 : obj) : obj :=
      let x_2 : tagged := 0;
      let x_3 : u32 := 32;
      let x_4 : obj := String.set x_1 x_2 x_3;
      let x_5 : obj := process._closed_0;
      inc x_5;
      let x_6 : obj := ctor_0[Prod.mk] x_4 x_5;
      ret x_6

另一方面,process' 的 IR 在调用 String.set 之前递增字符串的引用计数。 因此,修改后的字符串 x_4 是一个副本,无论对 x_1 的原始引用是否唯一:

[Compiler.IR] [result]
    def process' (x_1 : obj) : obj :=
      let x_2 : tagged := 0;
      let x_3 : u32 := 32;
      inc x_1;
      let x_4 : obj := String.set x_1 x_2 x_3;
      let x_5 : obj := ctor_0[Prod.mk] x_4 x_1;
      ret x_5
Memory Reuse in IR

函数 discardElemsList.map 的简化版本,它将列表中的每个元素替换为 ()。 检查其中间表示表明,当其引用唯一时,它将重用列表的内存。

set_option trace.compiler.ir.result true def [Compiler.IR] [result] def discardElems._redArg (x_1 : tobj) : tobj := case x_1 : tobj of List.nil → let x_2 : tagged := ctor_0[List.nil]; ret x_2 List.cons → let x_3 : tobj := proj[1] x_1; block_4 (x_5 : tobj) (x_6 : u8) := let x_7 : tagged := ctor_0[PUnit.unit]; let x_8 : tobj := discardElems._redArg x_3; block_9 (x_10 : obj) := ret x_10; case x_6 : u8 of Bool.false → set x_5[1] := x_8; set x_5[0] := x_7; jmp block_9 x_5 Bool.true → let x_11 : obj := ctor_1[List.cons] x_7 x_8; jmp block_9 x_11; let x_12 : u8 := isShared x_1; case x_12 : u8 of Bool.false → let x_13 : tobj := proj[0] x_1; dec x_13; jmp block_4 x_1 x_12 Bool.true → inc x_3; dec x_1; jmp block_4 ◾ x_12[Compiler.IR] [result] def discardElems (x_1 : ◾) (x_2 : tobj) : tobj := let x_3 : tobj := discardElems._redArg x_2; ret x_3discardElems : List α List Unit | [] => [] | Variable name `x` is not explicitly referenced. The binding can be removed (if unused) or named `_` (if used implicitly). Note: This linter can be disabled with `set_option linter.unusedVariables false`x :: xs => () :: discardElems xs

这会发出以下 IR:

[Compiler.IR] [result]
    def discardElems._redArg (x_1 : tobj) : tobj :=
      case x_1 : tobj of
      List.nil →
        let x_2 : tagged := ctor_0[List.nil];
        ret x_2
      List.cons →
        let x_3 : tobj := proj[1] x_1;
        block_4 (x_5 : tobj) (x_6 : u8) :=
          let x_7 : tagged := ctor_0[PUnit.unit];
          let x_8 : tobj := discardElems._redArg x_3;
          block_9 (x_10 : obj) :=
            ret x_10;
          case x_6 : u8 of
          Bool.false →
            set x_5[1] := x_8;
            set x_5[0] := x_7;
            jmp block_9 x_5
          Bool.true →
            let x_11 : obj := ctor_1[List.cons] x_7 x_8;
            jmp block_9 x_11;
        let x_12 : u8 := isShared x_1;
        case x_12 : u8 of
        Bool.false →
          let x_13 : tobj := proj[0] x_1;
          dec x_13;
          jmp block_4 x_1 x_12
        Bool.true →
          inc x_3;
          dec x_1;
          jmp block_4 ◾ x_12[Compiler.IR] [result]
    def discardElems (x_1 : ◾) (x_2 : tobj) : tobj :=
      let x_3 : tobj := discardElems._redArg x_2;
      ret x_3

在 IR 中,List.cons 情况显式检查参数值是否共享(即其引用计数是否大于 1)。 如果引用是唯一的,则丢弃的列表元素 x_5 的引用计数将递减,并重用构造函数值。 如果共享,则在 x_11 中为结果分配一个新的 List.cons