堆 (Heap)
栈很棒,但它不能解决我们所有的问题。那些大小在编译期未知的数据怎么办? 集合 (collection)、字符串 (string) 以及其他动态尺寸 (dynamically-sized) 的数据无法(完整地)放到栈上。 这就轮到堆 (heap) 登场了。
堆分配 (Heap allocations)
你可以把堆想象成一大块内存——如果愿意的话,可以把它当成一个巨大的数组。
每当你需要在堆上存储数据时,你向一个特殊的程序——分配器 (allocator)——请求为你预留堆中的一部分。我们把这种交互(以及你预留的那块内存)称作一次堆分配 (heap allocation)。
如果分配成功,分配器会给你一个指向所预留区块起始位置的指针 (pointer)。
没有自动释放 (No automatic de-allocation)
堆的结构与栈相当不同。
堆分配不是连续的,它们可以位于堆中的任意位置。
+---+---+---+---+---+---+-...-+-...-+---+---+---+---+---+---+---+
| Allocation 1 | Free | ... | ... | Allocation N | Free |
+---+---+---+---+---+---+ ... + ... +---+---+---+---+---+---+---+
哪些区域在使用、哪些是空闲的,由分配器来跟踪。 不过分配器不会自动释放你分配的内存:你需要主动行事,再次调用分配器来释放 (free) 你不再需要的内存。
性能 (Performance)
堆的灵活性是有代价的:堆分配比栈分配更慢。
其中涉及大量的簿记!
如果你读过性能优化方面的文章,常会看到这样的建议:尽量减少堆分配,能用栈分配的就用栈。
String 的内存布局 (String's memory layout)
当你创建一个 String 类型的局部变量时,
Rust 不得不在堆上分配1:它无法事先知道你要往里塞多少文本,
所以无法在栈上提前预留合适的空间。
但 String 并不是_完全_位于堆上,它在栈上也保留了一些数据。具体来说:
- 指向你预留的堆区域的指针 (pointer)。
- 字符串的长度 (length),即字符串当前包含的字节数。
- 字符串的容量 (capacity),即在堆上预留了多少字节。
我们看个例子来更好理解:
let mut s = String::with_capacity(5);
如果你运行这段代码,内存布局会是这样:
+---------+--------+----------+
Stack | pointer | length | capacity |
| | | 0 | 5 |
+--|------+--------+----------+
|
|
v
+---+---+---+---+---+
Heap: | ? | ? | ? | ? | ? |
+---+---+---+---+---+
我们请求了一个最多能装 5 字节文本的 String。
String::with_capacity 向分配器请求 5 字节的堆内存。分配器返回一个指向那块内存起始位置的指针。
不过此时 String 是空的。在栈上,我们通过区分长度 (length) 和容量 (capacity) 来记录这一信息:这个 String 最多可装 5 字节,但当前只装了 0 字节实际文本。
如果你往 String 里塞一些文本,情况就变了:
s.push_str("Hey");
+---------+--------+----------+
Stack | pointer | length | capacity |
| | | 3 | 5 |
+--| ----+--------+----------+
|
|
v
+---+---+---+---+---+
Heap: | H | e | y | ? | ? |
+---+---+---+---+---+
s 现在装了 3 字节的文本。它的长度更新为 3,但容量仍是 5。
堆上 5 个字节中有 3 个被用来存储字符 H、e 和 y。
usize
我们要在栈上存指针、长度和容量,需要多大空间?
这取决于你运行机器的架构 (architecture)。
机器上的每个内存位置都有一个地址 (address),通常表示为一个无符号整数。 根据地址空间的最大大小(即你的机器能寻址多少内存)不同, 这个整数可能有不同的大小。大多数现代机器使用 32 位或 64 位的地址空间。
Rust 通过提供 usize 类型来抽象掉这些与架构相关的细节:
一个无符号整数,其大小恰好等于在你机器上寻址内存所需的字节数。
在 32 位机器上,usize 等价于 u32;在 64 位机器上,对应 u64。
容量、长度和指针在 Rust 中都用 usize 表示2。
堆上没有 std::mem::size_of (No std::mem::size_of for the heap)
std::mem::size_of 返回某个类型在栈上会占用多少空间,
也称为类型的大小 (size of the type)。
那么
String在堆上管理的内存缓冲区呢?它不算String大小的一部分吗?
不算!
那块堆分配是 String 所管理的资源。
编译器并不把它看作 String 类型的一部分。
std::mem::size_of 不知道(也不在乎)某个类型可能管理或通过指针引用的额外堆内存——String 就属于这种情况——
因此它不会跟踪这部分大小。
不幸的是,目前没有等价于 std::mem::size_of 的工具来在运行时测量某个值正在分配多少堆内存。
某些类型可能提供方法来查看它们的堆使用情况(例如 String 的 capacity 方法),
但 Rust 没有通用的"API"来在运行时获取堆使用情况。
不过你可以用内存分析工具(例如 DHAT
或自定义分配器 (custom allocator))来检查程序的堆使用情况。
如果你创建一个空 String(即 String::new()),std 不会进行堆分配。
当你第一次往里塞数据时,才会预留堆内存。
指针的大小还取决于操作系统。 在某些环境下,指针比内存地址更大(例如 CHERI)。 Rust 做了一个简化假设——指针和内存地址同样大——这对你大多数现代系统来说是成立的。
原文链接:英文原文