11|内存管理:从创建到消亡,值都经历了什么?

2,901 阅读5分钟

正式开始

Rust内存管理

  1. 大部分堆内存的需求在于动态大小,小部分需求是更长的生命周期
  2. Rust默认将堆内存的生命周期和使用它的栈内存的生命周期绑在一起,并留了个小口子 leaked 机制,让堆内存在需要的时候,可以有超出帧存活期的生命周期

image.png

值的创建

  1. 编译时可以确定大小的值都会放在栈上,包括 Rust 提供的原生类型比如字符、数组、元组(tuple)等,以及开发者自定义的固定大小的结构体(struct)、枚举(enum) 等
  2. 如果数据结构的大小无法确定,或者它的大小确定但是在使用时需要更长的生命周期,就最好放在堆上

struct

  1. Rust 在内存中排布数据时,会根据每个域的对齐(aligment)对数据进行重排,使其内存大小和访问效率最好

  2. C 语言会对结构体对齐的规则

    a. 首先确定每个域的长度和对齐长度,原始类型的对齐长度和类型的长度一致

    b. 每个域的起始位置要和其对齐长度对齐,如果无法对齐,则添加 padding 直至对齐

    c. 结构体的对齐大小和其最大域的对齐大小相同,而结构体的长度则四舍五入到其对齐的倍数

image.png

Rust 编译器默认为开发者优化结构体的排列,但你也可以 使用#[repr] 宏,强制让 Rust 编译器不做优化 ,和 C 的行为一致,这样,Rust 代码可以方便地和 C 代码无缝交互

enum

  1. 在 Rust 下它是一个标签联合体(tagged union),它的大小是标签的大小加上最大类型的长度
  2. 根据刚才说的三条对齐规则,tag 后的内存,会根据其对齐大小进行对齐。一般而言,64 位 CPU 下,enum 的最大长度是:最大类型的长度 + 8

image.png

以下代码可以打印常见数据结构的大小

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

  1. Vec<T> 结构是 3 个 word 的胖指针

    a. 指向堆内存的指针 pointer

    b. 分配的堆内存的容量 capacity

    c. 数据在堆内存的长度 length

    image.png

  2. 很多动态大小的数据结构,在创建时都有类似的内存布局:栈内存放的胖指针,指向堆内存分配出来的数据

值的使用

  1. 对 Rust 而言,一个值如果没有实现 Copy,在赋值、传参以及函数返回时会被 Move
  2. Copy 和 Move 在内部实现上,都是浅层的按位做内存复制,只不过 Copy 允许你访问之前的变量,而 Move 不允许

image.png

  1. 无论是 Copy 还是 Move,它的效率都是非常高的
  2. 一般我们建议在栈上不要放大数组
  3. 动态数组数据增加会导致内存使用率低下,使用 shrink_to_fit 节约内存

值的销毁

当一个值要被释放,它的 Drop trait 会被调用

简单类型的释放

image.png

  1. 变量 greeting 是一个字符串,在退出作用域时,其 drop() 函数被自动调用
  2. 释放堆上包含 “hello world” 的内存
  3. 然后再释放栈上的内存

复杂数据结构的释放

  1. 比如一个结构体,那么这个结构体在调用 drop() 时,会依次调用每一个域的 drop() 函数
  2. 如果域又是一个复杂的结构或者集合类型,就会递归下去,直到每一个域都释放干净

image.png

  1. student 变量是一个结构体,有 name、age、scores
  2. 其中 name 是 String,scores 是 HashMap,它们本身需要额外 drop()
  3. 又因为 HashMap 的 key 是 String,所以还需要进一步调用这些 key 的 drop()
  4. 整个释放顺序从内到外是:先释放 HashMap 下的 key,然后释放 HashMap 堆上的表结构,最后释放栈上的内存

堆内存释放

所有权机制规定了,一个值只能有一个所有者,所以在释放堆内存的时候,就是单纯调用 Drop trait

释放其他资源

Rust 对所有的资源都有很好的 RAII 支持。

小结

image.png

好用链接

  1. 数据对齐
  2. String 源码
  3. Vec<T>结构 源码
  4. Rust cheats 快速手册
  5. RAII
  6. RFC
  7. 生命周期概念 帖子

精选问答

  1. Result<String, ()> 占用多少内存?为什么?

    a. 引用类型的第一个域是个指针,而指针是不可能等于 0 的,我们可以复用这个指针:当其为 0 时,表示 None,否则是 Some

    b. 对于 Result<String, ()> 也是如此,String 第一个域是指针,而指针不能为空,所以当它为空的时候,正好可以表述 Err(())

  2. 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 编译