Appearance
20.1 内存申请
首先,我们知道 Vec 是一个动态数组,它会根据情况动态扩展当前的空间大小。 在 Rust 以及 C++ 这样的语言中,动态数组这样的类型是由库来实现的,而不是像某些带 GC 的语言一样由运行时环境内置提供。 这是因为 Rust 和 C++ 一样,都具备对内存的直接控制力,其中一个表现就是,可以手动调用内存分配器,自己管理内存分配和释放的策略。
动态数组类型的基本思想是:它不像内置数组一样直接把数据保存到栈上,而是在堆上开辟一块空间来保存数据,在向 Vec 中插入数据的时候,如果当前已分配的空间不够用了,它会重新分配更大的内存空间,把原有的数据复制过去,然后继续执行插入操作。
在目前版本的标准库中,Vec 类型的源码存在于alloc/vec/mod.rs
文件中。 它的定义很简单:
rust
pub struct Vec<T, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global> {
buf: RawVec<T>,
len: usize,
}
它只有两个成员:一个是RawVec<T>
类型,管理内存空间的分配和释放;另外一个是usize
类型,记录当前包含的元素个数。 Vec 的 new 和 capacity 方法都很简单:
rust
pub fn new() -> Vec<T> {
Vec {
buf: RawVec::new(),
len: 0,
}
}
pub fn with_capacity(capacity: usize) -> Vec<T> {
Vec {
buf: RawVec::with_capacity(capacity),
len: 0,
}
}
我们继续深入查看 RawVec 这个类型的 new 和 capacity 方法是如何实现的。 它的源码在alloc/raw_vec.rs
文件中:
rust
#[allow(missing_debug_implementations)]
pub(crate) struct RawVec<T, A: Allocator = Global> {
ptr: Unique<T>,
cap: usize,
alloc: A,
}
...
impl<T> RawVec<T, Global> {
pub const fn new() -> Self {
Self::new_in(Global)
}
pub fn with_capacity(capacity: usize) -> Self {
Self::with_capacity_in(capacity, Global)
}
}
从这里可以看到,RawVec
的泛型参数比Vec
多了一个,这个泛型参数代表的是内存分配器 allocator。 这个设计的目的是让 Rust 的标准容器能像 C++ 中的一样,可以由用户自行指定内存分配器。 这个功能目前还处于设计过程中,因此只有 RawVec 中有这个功能,而 Vec 还没有,以后 Vec 同样也会有这样一个泛型参数的。 这个泛型参数有一个默认值,叫作 Global,是标准库给我们提供的默认内存分配器。一般来说,内存分配器都是0
大小的类型,它没有任何成员,不保存任何状态信息。 比如 Global 这个类型的定义:
rust
#[unstable(feature = "allocator_api", issue = "32838")]
#[derive(Copy, Clone, Default, Debug)]
#[cfg(not(test))]
pub struct Global;
继续分析RawVec
的new
方法。它调用了它自己的new_in
方法,并将 allocator 作为参数传递进去。 因为 Global 类型没有成员,所以它可以直接用它的名字当作一个对象实例来使用。 这个new_in
方法是这样实现的:
rust
/// Like `new`, but parameterized over the choice of allocator for
/// the returned `RawVec`.
pub const fn new_in(alloc: A) -> Self {
// `cap: 0` means "unallocated". zero-sized types are ignored.
Self { ptr: Unique::dangling(), cap: 0, alloc }
}
对于成员ptr
以及成员alloc
,都是简单的默认构造。 只有成员cap
的大小是0
,那么显然 Vec 即便不申请任何内存,也可以存下任意多的 T 类型成员。 因为不管你往 Vec 中插入多少数据,总大小依然是0
。所以这里的处理逻辑就是,当size_of::<T>()==0
的时候,cap
的取值是usize::MAX
。 这里的!0
的写法,实际上是对0
按位取反。
再回看 RawVec 的with_capacity
方法。它调用了它自己的allocate_in
方法。
这个方法的实现如下所示:
rust
fn allocate_in(cap: usize, zeroed: bool, mut a: A) -> Self {
unsafe {
let elem_size = mem::size_of::<T>();
let alloc_size = cap.checked_mul(elem_size).expect("capacity overflow");
alloc_guard(alloc_size);
// handles ZSTs and `cap = 0` alike
let ptr = if alloc_size == 0 {
mem::align_of::<T>() as *mut u8
} else {
let align = mem::align_of::<T>();
let result = if zeroed {
a.alloc_zeroed(Layout::from_size_align(alloc_size, align).unwrap())
} else {
a.alloc(Layout::from_size_align(alloc_size, align).unwrap())
};
match result {
Ok(ptr) => ptr,
Err(err) => a.oom(err),
}
};
RawVec {
ptr: Unique::new_unchecked(ptr as *mut _),
cap,
a,
}
}
}
#[cfg(not(no_global_oom_handling))]
fn allocate_in(capacity: usize, init: AllocInit, alloc: A) -> Self {
// Don't allocate here because `Drop` will not deallocate when `capacity` is 0.
if T::IS_ZST || capacity == 0 {
Self::new_in(alloc)
} else {
// We avoid `unwrap_or_else` here because it bloats the amount of
// LLVM IR generated.
let layout = match Layout::array::<T>(capacity) {
Ok(layout) => layout,
Err(_) => capacity_overflow(),
};
match alloc_guard(layout.size()) {
Ok(_) => {}
Err(_) => capacity_overflow(),
}
let result = match init {
AllocInit::Uninitialized => alloc.allocate(layout),
AllocInit::Zeroed => alloc.allocate_zeroed(layout),
};
let ptr = match result {
Ok(ptr) => ptr,
Err(_) => handle_alloc_error(layout),
};
// Allocators currently return a `NonNull<[u8]>` whose length
// matches the size requested. If that ever changes, the capacity
// here should change to `ptr.len() / mem::size_of::<T>()`.
Self {
ptr: unsafe { Unique::new_unchecked(ptr.cast().as_ptr()) },
cap: capacity,
alloc,
}
}
}
首先计算需要分配的内存大小。它使用了checked_mul
处理溢出问题。然后考虑0
大小的问题。 此时无须调用内存分配器的方法,而是直接返回一个数值很小的指针(一般情况下这个值就是 1)。 为了有利于编译器后端优化,这个指针保证了与 T 类型字节对齐。此时不是直接用数值 0,主要是为了和“空指针”做区分。 因为 RawVec 已经假定了它的成员 ptr 永远不会是空指针,所以它用了 Unique 类型。 这种设计可以让Option<Vec<T>>
拥有和Vec<T>
相同的大小,而无须浪费空间。
最后我们再来分析一下 RawVec 里面的ptr
成员。它是Unique<T>
这个类型。
这个类型的定义如下:
rust
pub struct Unique<T: ?Sized> {
pointer: NonZero<*const T>,
_marker: PhantomData<T>,
}
这个类型是在裸指针基础上做了一点封装。
它通过
PhantomData<T>
方式,向编译器表达了“它是 T 类型对象的拥有者”这样一个概念。PhantomData
这个类型是一个 0 大小的、被编译器特殊对待的类型,它有一个 attribute 做修饰#[lang="phantom_data"]
,凡是被#[lang=…]
修饰的东西,都是被编译器特殊处理的,跟普通用户自己定义的不一样。因为从逻辑上说这个指针不应该为空,因此它使用 NonZero 做了一个包装。NonZero 这个类型也是一个特殊类型,它也有一个 attribute 是
#[lang="non_zero"]
。 在编译器内部,会认为这个类型的取值永远不可能为 0。 这样在某些情况下,编译器可以根据这个信息优化存储空间。比如Option<Box<T>>
占据的空间大小跟Box<T>
就一模一样,无须额外空间,这里的关键就是Box<T>
内部也使用了 NonZero 这个类型。Unique<T>
还实现了 Send 和 Sync 这两个 trait。unsafe impl<T:Send+?Sized> Send for Unique<T>{}
unsafe impl<T:Sync+?Sized> Sync for Unique<T>{}
标准库中很多底层的数据结构都需要基于裸指针,使用 unsafe 代码才能实现。 而很多数据结构都需要表达一种“对成员拥有所有权”这样一个概念,因此它们有一些共同的代码可以复用。 这就是为什么 RawVec 是基于 Unique 类型而不是直接基于裸指针来实现的原因。 因为抽象出的这样一个 Unique 类型,不止在实现动态数组的时候有用,还可以在实现Box<T>HashMap<K,V>
等类型的时候有用。
与之对应的,标准库中还有一个叫作Shared<T>
的类型。它也是在裸指针基础上做了一点封装。 它跟Unique<T>
之间的主要区别在于,Shared<T>
适合用于表达那种“共享所有权的引用”的情况。 比如Rc<T>
Arc<T>
LinkedList<T>
等类型,都是基于Shared<T>
实现的。