Skip to content
📖 Auto 编程语言(主书)本书目录

内存与所有权 ​

每个程序都必须管理内存——在需要时为数据分配空间,在不再使用时释放该空间。不同 语言采取了截然不同的方法。C 提供 malloc 和 free 并相信你不会犯错。Java 和 Python 使用垃圾回收(GC),运行一个后台进程定期扫描未使用的对象。Rust 和 Auto 使用第三种方法:所有权——一组编译时规则,保证内存安全且零运行时开销。

在本章中,我们将讨论:

  • 值的存储位置 —— 栈与堆分配
  • 隐式移动语义 —— 无需仪式的所有权转移
  • 克隆 —— 使用 .clone() 显式深拷贝
  • 逃逸分析 —— 编译器决定栈还是堆
  • AutoFree —— 无需垃圾回收的自动清理
  • 零 GC 实践 —— 性能影响

值的存储位置 ​

Auto 程序中的内存分为两个区域:栈和堆。理解这种区别对于理解所有权至 关重要,因为每个区域的规则不同。

栈是一个后进先出(LIFO)的数据结构。局部变量在函数调用时被压入栈,在函数 返回时弹出。栈分配很快——栈指针只需移动——而释放是瞬时的。栈上的每个值在编译 时都有已知的固定大小。

堆是一个结构较少的区域。当你需要编译时大小未知的内存(动态大小的数组、 可增长的字符串、节点树)时,你向堆分配器请求。分配器找到一个空位,将其标记 为已使用,并返回一个指针。堆分配较慢,访问堆数据需要跟随指针。

栈与堆:值的存储位置

输出:

5
hello
[1, 2, 3]

三个变量,三种不同的存储策略:

  • x 保存整数 5。整数有固定大小(i64 为 8 字节),所以它们存储在栈上。 不需要堆分配。

  • s 保存字符串 "hello"。Auto 中的短字符串使用_小字符串优化_(SSO) ——当字符串数据足够小时,直接存储在栈上,避免堆分配。较长的字符串会自动 移到堆上。

  • list 保存数组 [1, 2, 3]。数组是动态大小的,在堆上分配。变量 list 本身在栈上,保存指向堆数据的指针。

在各语言中的映射:

  • Rust 与 Auto 行为相同。i64 仅在栈上,String 使用 SSO,Vec<i64> 在堆上分配。
  • Python 将所有对象存储在堆上并使用引用。这种区别对程序员基本不可见。
  • C 给你完全控制权。long long 在栈上,const char* 指向二进制中的 只读数据,数组可以在栈上或堆上,取决于你如何分配。
  • TypeScript 依赖 V8 引擎的内存管理。所有值概念上都在堆上,引擎在可能 的地方进行优化。

隐式移动语义 ​

在 Auto 中,每个值恰好有一个_所有者_——负责在不再需要时清理它的变量。当你 将值传递给函数或赋值给新变量时,Auto 默认_移动_所有权。原始变量变为无效。

隐式移动——所有权转移到函数

输出:

Hello

当调用 take_ownership(greeting) 时,Auto 将字符串的所有权从 greeting 转移到函数的参数 s。调用之后,greeting 不再有效。尝试使用它(被注释掉 的行)会产生编译时错误。

这是 Auto 中等价于 Rust 移动语义的概念。关键洞察是始终恰好有一个所有者。 当所有权被转移时,旧所有者变为无效。这防止了_双重释放_ bug——两个变量都试 图释放同一块堆内存的情况。

在各语言中的映射:

  • Rust 强制执行相同的移动语义。将 String 传递给函数会移动它,原始变 量变为无效。
  • Python 没有移动语义。字符串是由引用计数管理的不可变对象。原始变量保 持有效。
  • C 没有内置的移动语义。转译器在 take_ownership 中生成显式的 free(s) 来模拟所有权转移。如果你在调用后尝试使用 greeting,你会得到 一个使用后释放的 bug——运行时崩溃。
  • TypeScript 不强制执行移动语义。注释纯粹是文档性质的。

克隆 ​

如果你需要在将值传递给函数后仍然使用它,有两个选择:用 view T 借用它(在 第 11 章中介绍),或用 .clone() 克隆它。克隆创建值的深拷贝——一个完全独立 的副本,拥有自己的堆内存。

使用 `.clone()` 保留原始值

输出:

Hello
Hello

greeting.clone() 创建字符串的完整副本。副本被传递给 take_ownership, 该函数取得副本的所有权并在完成后释放它。原始的 greeting 保持有效,因为它 从未被移动——只有克隆体被移动了。

在 Auto(和 Rust)中,克隆是显式的,因为它有代价。当你在代码中看到 .clone() 时,你知道可能发生了一次昂贵的内存分配和复制。这使得对性能关键 代码的审计更容易:只需搜索 .clone() 调用,看看它们是否必要。

在各语言中的映射:

  • Rust 使用相同的 .clone() 方法。约定完全相同——克隆总是显式的。
  • Python 字符串是不可变的,所以"克隆"实际上是免费的(只是复制引用)。 转译器使用 [:] 切片来使意图更清晰。
  • C 使用 strdup() 创建副本。调用者必须记住同时释放原始值和克隆体。 这是内存泄漏的常见来源。
  • TypeScript 字符串是不可变的基本类型,所以 .slice() 创建副本(尽 管引擎可能会优化掉它)。

逃逸分析 ​

在许多情况下,Auto 可以在编译时确定一个值是需要存储在堆上还是可以安全地 留在栈上。这种优化称为_逃逸分析_——编译器检查一个值是否"逃逸"了创建它的 作用域。

如果一个值在函数内部创建并返回给调用者,它就逃逸了。如果它在函数内部创建 并完全在函数内部使用,它不会逃逸,可以留在栈上。

逃逸分析——编译器决定栈还是堆

输出:

1
2

make_point 创建一个 Point 结构体并返回它。Point 有固定大小(两个 f64 字段,总共 16 字节)。编译器可以看到该值不需要堆分配——它按值返回并 存储在栈上的 p 中。

在 Rust 和 Auto 中,编译器执行逃逸分析来确定最佳存储位置:

  • 从函数返回的值:如果值有固定大小,可以按值返回(不需要堆分配)。这 是已知大小结构体的常见情况。
  • 生命周期超出当前作用域的值:如果一个值存储在集合中、传递给线程或被闭 包捕获,它可能需要堆分配。编译器会自动处理。
  • 动态大小的值:列表、字符串和哈希映射总是需要堆分配,因为它们的大小 在编译时未知。

C 代码展示了相同的模式——make_point 按值返回 Point 结构体,编译器使 用返回值优化(RVO)来消除不必要的复制。

为什么逃逸分析很重要 ​

没有逃逸分析,每个从函数返回的值都需要在堆上分配,每个返回的值都需要垃圾 回收器或手动 free。有了逃逸分析,编译器可以在可能的情况下将值保持在栈 上,减少内存压力并提高缓存性能。

AutoFree ​

当一个拥有的值离开作用域时,Auto 自动释放其内存。这称为 AutoFree——编 译器在每个变量生命周期结束的位置插入清理代码。你永远不需要调用 free()、 delete 或 dispose()。编译器来处理。

AutoFree——无需垃圾回收的自动清理

输出:

Buffers allocated and auto-freed

b1 和 b2 是栈分配的结构体。当 main 返回时,编译器自动插入清理代码。 对于像 Buffer 这样只包含栈类型的简单结构体,清理是一个空操作——栈指针只 需回移。对于包含堆数据的类型(如 String 或 List),编译器插入 free() 调用来释放堆内存。

这与垃圾回收有本质区别:

  • 垃圾回收在后台定期运行,扫描不再可达的对象。这引入了_暂停时间_—— 程序停止收集垃圾的时刻。暂停的持续时间和时机是不可预测的。

  • AutoFree在每个作用域结束时确定性运行。内存在变量离开作用域的瞬间被释 放。没有暂停时间,没有后台线程,没有运行时开销。

在各语言中的映射:

  • Rust 使用相同的机制(称为 Drop)。当值离开作用域时,其 Drop 实现 被自动调用。
  • Python 使用引用计数加循环检测器。当对象的引用计数降为零时释放对象, 精神上类似但增加了运行时开销。
  • C 需要手动 free() 调用。忘记释放导致内存泄漏;双重释放导致崩溃。 这里的 C 代码很简单,因为 Buffer 在栈上分配,不需要显式清理。
  • TypeScript 依赖 V8 引擎的垃圾回收器。程序员无法控制何时释放内存。

零 GC 实践 ​

所有权、移动语义和 AutoFree 的结合意味着 Auto 程序在没有垃圾回收器的情况 下运行。这具有真实的性能影响:可预测的延迟、一致的吞吐量和更低的内存开销。

零 GC 实践——无垃圾回收暂停

输出:

Done processing 1M items

这个循环创建并销毁了一百万个数组。在垃圾回收语言中,这会产生显著的 GC 压力 ——回收器需要定期运行来清理被丢弃的数组,导致不可预测的暂停。

在 Auto(和 Rust)中,每个数组在循环迭代结束时立即被释放。分配和释放是确 定性的:分配器在迭代开始时分配,在迭代结束时释放。没有后台线程,没有 stop-the-world 暂停,没有内存压力积累。

C 代码展示了相同的模式——开始时 malloc,结束时 free。但在 C 中,你必须 记住每个 free()。忘记一个会导致泄漏。释放同一个指针两次会导致崩溃。Auto 的所有权系统保证每个分配恰好被释放一次。

真实世界的影响 ​

零 GC 不仅仅是理论上的优势。它在以下场景中很重要:

  • 游戏开发:帧时间必须一致。渲染帧中间的 GC 暂停会导致可见的卡顿。
  • 实时系统:音频处理、视频编码和嵌入式控制器无法容忍不可预测的暂停。
  • 服务器软件:请求延迟必须可预测。Java 服务器中的 GC 暂停是尾延迟的 著名来源。
  • 移动应用:GC 暂停导致 UI 渲染中的卡顿,并通过保持 CPU 核心活跃 进行回收来消耗电池。

总结 ​

Auto 的内存管理建立在三大支柱上:

概念机制运行时开销
所有权每个值有一个所有者无(编译时)
移动语义赋值/函数调用时转移所有权无(编译时)
AutoFree作用域结束时清理无(确定性)

与其他方法的比较:

语言内存管理安全性可预测性
C手动 malloc/free不安全高
Java/Python/TS垃圾回收安全低(GC 暂停)
Rust/Auto所有权 + AutoFree安全高

所有权模型为你提供了垃圾回收语言的安全性,同时拥有手动内存管理的性能和可预 测性。编译器完成簿记工作——你只需编写代码。

在下一章中,我们将讨论 Auto 的集合类型——List、Map 和 Set——它们建 立在这些所有权原则之上,提供安全、高效的动态数据结构。

基于 MIT 许可发布。