正式开始
Rust内存管理
- 大部分堆内存的需求在于动态大小,小部分需求是更长的生命周期
- Rust默认将堆内存的生命周期和使用它的栈内存的生命周期绑在一起,并留了个小口子 leaked 机制,让堆内存在需要的时候,可以有超出帧存活期的生命周期
值的创建
- 编译时可以确定大小的值都会放在栈上,包括 Rust 提供的原生类型比如字符、数组、元组(tuple)等,以及开发者自定义的固定大小的结构体(struct)、枚举(enum) 等
- 如果数据结构的大小无法确定,或者它的大小确定但是在使用时需要更长的生命周期,就最好放在堆上
struct
-
Rust 在内存中排布数据时,会根据每个域的对齐(aligment)对数据进行重排,使其内存大小和访问效率最好
-
C 语言会对结构体对齐的规则
a. 首先确定每个域的长度和对齐长度,原始类型的对齐长度和类型的长度一致
b. 每个域的起始位置要和其对齐长度对齐,如果无法对齐,则添加 padding 直至对齐
c. 结构体的对齐大小和其最大域的对齐大小相同,而结构体的长度则四舍五入到其对齐的倍数
Rust 编译器默认为开发者优化结构体的排列,但你也可以 使用#[repr] 宏,强制让 Rust 编译器不做优化 ,和 C 的行为一致,这样,Rust 代码可以方便地和 C 代码无缝交互
enum
- 在 Rust 下它是一个标签联合体(tagged union),它的大小是标签的大小加上最大类型的长度
- 根据刚才说的三条对齐规则,tag 后的内存,会根据其对齐大小进行对齐。一般而言,64 位 CPU 下,enum 的最大长度是:最大类型的长度 + 8
以下代码可以打印常见数据结构的大小
use std::collections::HashMap;
use std::mem::size_of;
enum E {
A(f64),
B(HashMap<String, String>),
C(Result<Vec<u8>, String>),
}
// 这是一个声明宏,它会打印各种数据结构本身的大小,在 Option 中的大小,以及在 Result 中的大小
macro_rules! show_size {
(header) => {
println!(
"{:<24} {:>4} {} {}",
"Type", "T", "Option<T>", "Result<T, io::Error>"
);
println!("{}", "-".repeat(64));
};
($t:ty) => {
println!(
"{:<24} {:4} {:8} {:12}",
stringify!($t),
size_of::<$t>(),
size_of::<Option<$t>>(),
size_of::<Result<$t, std::io::Error>>(),
)
};
}
fn main() {
show_size!(header);
show_size!(u8);
show_size!(f64);
show_size!(&u8);
show_size!(Box<u8>);
show_size!(&[u8]);
show_size!(String);
show_size!(Vec<u8>);
show_size!(HashMap<String, String>);
show_size!(E);
}
/*
Type T Option<T> Result<T, io::Error>
----------------------------------------------------------------
u8 1 2 16
f64 8 16 16
&u8 8 8 16
Box<u8> 8 8 16
&[u8] 16 16 24
String 24 24 32
Vec<u8> 24 24 32
HashMap<String, String> 48 48 56
E 56 56 64
*/
Option 配合带有引用类型的数据结构,比如 &u8、Box、Vec、HashMap ,没有额外占用空间
Option 复用了引用类型的第一个域(是个指针),当其为 0 时表示 None
Option<&u8>中,将指针作为tag使用,指针为0时,表示没有数据,也即None; 否则,表示有数据,则是Some
vec<T> 和 String
-
Vec<T> 结构是 3 个 word 的胖指针
a. 指向堆内存的指针 pointer
b. 分配的堆内存的容量 capacity
c. 数据在堆内存的长度 length
-
很多动态大小的数据结构,在创建时都有类似的内存布局:栈内存放的胖指针,指向堆内存分配出来的数据
值的使用
- 对 Rust 而言,一个值如果没有实现 Copy,在赋值、传参以及函数返回时会被 Move
- Copy 和 Move 在内部实现上,都是浅层的按位做内存复制,只不过 Copy 允许你访问之前的变量,而 Move 不允许
- 无论是 Copy 还是 Move,它的效率都是非常高的
- 一般我们建议在栈上不要放大数组
- 动态数组数据增加会导致内存使用率低下,使用 shrink_to_fit 节约内存
值的销毁
当一个值要被释放,它的 Drop trait 会被调用
简单类型的释放
- 变量 greeting 是一个字符串,在退出作用域时,其 drop() 函数被自动调用
- 释放堆上包含 “hello world” 的内存
- 然后再释放栈上的内存
复杂数据结构的释放
- 比如一个结构体,那么这个结构体在调用 drop() 时,会依次调用每一个域的 drop() 函数
- 如果域又是一个复杂的结构或者集合类型,就会递归下去,直到每一个域都释放干净
- student 变量是一个结构体,有 name、age、scores
- 其中 name 是 String,scores 是 HashMap,它们本身需要额外 drop()
- 又因为 HashMap 的 key 是 String,所以还需要进一步调用这些 key 的 drop()
- 整个释放顺序从内到外是:先释放 HashMap 下的 key,然后释放 HashMap 堆上的表结构,最后释放栈上的内存
堆内存释放
所有权机制规定了,一个值只能有一个所有者,所以在释放堆内存的时候,就是单纯调用 Drop trait
释放其他资源
Rust 对所有的资源都有很好的 RAII 支持。
小结
好用链接
精选问答
-
Result<String, ()> 占用多少内存?为什么?
a. 引用类型的第一个域是个指针,而指针是不可能等于 0 的,我们可以复用这个指针:当其为 0 时,表示 None,否则是 Some
b. 对于 Result<String, ()> 也是如此,String 第一个域是指针,而指针不能为空,所以当它为空的时候,正好可以表述 Err(())
-
rust 中的 feature 是干什么用的,怎么开发?
a. feature 用作条件编译,你可以根据需要选择使用库的某些 feature。它的好处是可以让编译出的二进制比较灵活,根据需要装入不同的功能
b. 定义 feature,你可以看 cargo book:doc.rust-lang.org/cargo/refer…
c. 以下是个简单例子
在 cargo.toml 中,可以定义: [features] filter = ["futures-util"] // 定义 filter feature,它有额外对 futures-util 的依赖。 [dependencies] futures-util = { version = "0.3", optional = true } // 这个 dep 声明成 optional 在 lib.rs 中: #[cfg(feature = "filter")] pub mod filter_all; // 只有编译 feature filter 时,才引入 mod feature_all 编译