std::vec::Vec 的基本法
- 2019-07-15
- Phosphorus15
众所周知,std::vec::Vec
是Rust中最常用的一种变长数组实现。由于实现了诸多特性,并且可以被方便地解构成Slice
,在生产环境中被广泛应用。
和Rust
所保证的一致,在常规情况下使用Vec
的实现,是不会(或者说极少会)导致内存泄露等问题的。但同时,Vec
本身的实现也提供了一些非安全(unsafe)的方法,在此假设读者或多或少已经接触过这些非安全方法,在此基础上对Vec
进行介绍。
来自Vec
的基本法
很多使用者倾向于把Vec
当成黑箱,或者仅仅了解其内存的分配机制,实际上,能够在不了解这些的情况下安全地使用Vec
正是达到了其设计目的。但当对Vec
的非安全方法有需求时,明白其内部实现就至关重要。实际上,Rust
在官方文档中对其实现做了一系列的保证
(Guarantees),以防止开发者在非安全情况下使用Vec
时踩雷。
Vec
的结构
Rust
对向量结构体的成员结构做了保证——在任意情况下,Vec
均由一个三元组(Triplet)组成。这个三元组分别包含了向量的长度
(length)——指示目前向量内的有效元素数,容量
(capacity)——指示向量目前已分配的总空间,与指向数据的指针
(pointer)。并且有以下两点:
指针
(pointer)永远是非空的(空指针优化)- 不保证三元组变量的顺序
在Rust 1.34.0
的标准库中,Vec
的定义如下:
1 |
|
其中,Global
是一个默认的全局内存分配实现,其实际大小为0
。在RawVec
中不占任何空间。在上古版本aplha 1.0
中,有一个更加直观的简化定义
1 | pub struct Vec<T> { |
值得注意的是,两个版本虽然相近,但是len
和cap
的顺序却被调换了。
Unique<T>
也是一个很有意思的类型,在此可以暂时看作非空指针。
空间分配与管理
简单来说,向量的空间分配主要存在于两种情况——创建时和扩张时
在创建一个向量时,任何显式或隐式地指定一个空向量的行为会致使空间不被预先分配,此时ptr : Unique<T>
为空(即Unique::empty()
)。这些行为包括但不限于:
使用
Vec::new()
初始化使用
vec![]
宏调用
Vec::with_capacity(0)
用大小为
0
的类型作为Vec
的泛型参数按照
Rust
文档的定义,在创建时分配内存的前提是有mem::size_of::<T>() * capacity() > 0
以下是一个段证明代码
1 | pub struct Empty(); // 空类型 |
获得的输出如下:
1 | using vec![] |
从输出中,可以看出:
- 只有以非空的形式初始化向量,才会使向量分配有效内存,否则
Vec
中的指针为空 - 以空类型创建向量并不会导致任何空间分配的行为
- 以空类型创建的向量对应的
cap
是一个特殊值,对应的是usize::MAX
(即2^64 - 1
)
除了在创建时分配内存,向量扩张时,如果出现已分配内存不足的情况,将会触发空间分配。这种分配通常会导致内存拷贝的发生。例如,Vec::push
的实现有如下判断
1 | pub fn push(&mut self, value: T) { |
这意味着,当对向量元素进行追加时,如果分配的内存已经不足,向量将会自动扩张。
Rust
并未显式地保证任何扩张策略,但在目前的实现中,只有在临界条件下才会触发扩张,而且扩张的空间则是刚好够新增的元素使用。因此,在实际生产环境中,为了减少内存拷贝的触发次数,推荐预先指定好向量的容量,或者在适当时刻调用reserve
以及reserve_exact
来预分配内存。
内存的使用原则
在默认情况下,
Vec
会保证尽量最小的内存使用量1
2
3
4
5
6
7fn main() {
let mut v = vec![0];
let v2 = vec![0, 2, 3, 7];
v.extend(v2.iter());
println!("len of v: {}", v.len());
println!("capacity of v : {}", v.capacity());
}输出:
1
2len of v: 5
capacity of v : 5Vec
不会主动覆写任何已被移出向量元素原空间的内容,但也不保证这些空间不被用作其它用途1
2
3
4
5
6
7
8
9
10fn main() {
let mut v = vec![2333, 3333, 6666];
let pop_v3 = v.pop();
let ptr = v.as_ptr();
let v3 = unsafe {
std::ptr::read(ptr.add(2))
};
println!("{}", pop_v3.expect("should not panic"));
println!("{}", v3);
}输出:
1
26666
6666在保证写入内容有效的情况下,可以通过
unsafe
的方式在长度以外的范围写入数据,并手动将长度调整至此位置1
2
3
4
5
6
7
8
9
10
11fn main() {
let mut v: Vec<i32> = Vec::with_capacity(3);
unsafe {
let ptr = v.as_mut_ptr();
std::ptr::write(ptr, 5700);
std::ptr::write(ptr.add(1), 2600);
std::ptr::write(ptr.add(2), 3900);
v.set_len(3);
}
println!("{:?}", v)
}输出:
1
[5700, 2600, 3900]
不论何时,
Vec
所分配的内存都不会被优化到栈上,而是统一从堆上分配。(有一个crate
实现了这种优化,可以在GitHub上看到)不保证向量中元素被
drop
的顺序。