陈天 · Rust 编程第一课
陈天
Tubi TV 研发副总裁
23195 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 65 讲
基础篇 (21讲)
陈天 · Rust 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

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

文件、socket、锁等
留有 leaked 机制
默认将堆内存的生命周期和栈内存的生命周期绑在一起
释放其他资源
释放堆内存
类似面向对象编程中的析构函数
考虑使用 shrink_to_fit 方法
集合类型的自动扩展
高效的内存复制
浅层的按位内存复制
栈内存放胖指针,指向堆内存
Rust 编译器的额外优化
标签联合体
使用 #[repr] 宏强制关闭优化
Rust 编译器的优化
堆上放置动态大小或需要更长生命周期的值
栈上放置确定大小的值
Rust 的创新
解决了堆内存生命周期管理的问题
需要运行时自由操控
无法承载动态大小或生命周期超出帧存活范围的值
高效的分配和释放
数据结构的布局、Move/Copy、销毁过程的掌握
数据的创建、使用和销毁过程的理解
数据结构在内存中的布局对理解代码结构和效率有帮助
Drop trait
动态增长
Move 和 Copy
vec 和 String
enum
struct
数据结构在内存中的布局
堆内存
栈内存
参考资料
总结
值的销毁
值的使用
值的创建
堆内存管理
Rust 内存管理

该思维导图由 AI 生成,仅供参考

你好,我是陈天。
初探 Rust 以来,我们一直在学习有关所有权和生命周期的内容,想必现在,你对 Rust 内存管理的核心思想已经有足够理解了。
通过单一所有权模式,Rust 解决了堆内存过于灵活、不容易安全高效地释放的问题,既避免了手工释放内存带来的巨大心智负担和潜在的错误;又避免了全局引入追踪式 GC 或者 ARC 这样的额外机制带来的效率问题。
不过所有权模型也引入了很多新概念,从 Move / Copy / Borrow 语义到生命周期管理,所以学起来有些难度。
但是,你发现了吗,其实大部分新引入的概念,包括 Copy 语义和值的生命周期,在其它语言中都是隐式存在的,只不过 Rust 把它们定义得更清晰,更明确地界定了使用的范围而已
今天我们沿着之前的思路,先梳理和总结 Rust 内存管理的基本内容,然后从一个值的奇幻之旅讲起,看看在内存中,一个值,从创建到消亡都经历了什么,把之前讲的融会贯通。
到这里你可能有点不耐烦了吧,怎么今天又要讲内存的知识。其实是因为,内存管理是任何编程语言的核心,重要性就像武学中的内功。只有当我们把数据在内存中如何创建、如何存放、如何销毁弄明白,之后阅读代码、分析问题才会有一种游刃有余的感觉。

内存管理

确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Rust内存管理的核心思想是通过单一所有权模式解决堆内存释放问题,避免手动释放内存和全局引入追踪式GC或ARC带来的效率问题。本文深入浅出地介绍了Rust内存管理的基本内容,包括堆内存和栈内存的使用、对值的创建、结构体在内存中的布局和Rust编译器对结构体的自动优化,以及对enum的讨论。通过与C语言的对比,读者可以更好地理解Rust内存管理的特点和优势。此外,文章还介绍了enum、Option、Result、Vec和String等数据结构的内存布局和使用过程,以及值的销毁和堆内存释放的机制。Rust的内存管理设计简单清晰,使得大量简单的个体构造出高效且不出错的系统,具有独特的优势。另外,文章还探讨了Rust的Drop trait,它不仅可以释放堆内存,还可以释放任何资源,如文件、socket、锁等,使得资源管理更加优雅和安全。通过对内存管理的深入探讨,读者可以更好地理解数据结构在内存中的布局、Move和自动增长的过程,以及数据的销毁。这些内容有助于读者在阅读和编写代码时更加游刃有余。整体而言,本文为读者提供了深入了解Rust内存管理的良好基础,为进一步学习和应用Rust编程语言提供了重要参考。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《陈天 · Rust 编程第一课》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(30)

  • 最新
  • 精选
  • dotfiles
    置顶
    1. 每个内存对象仅有一个所有者 所有堆上的内存一定在栈上有对应的变量.堆上的内存是不能独立存在的,否则无法管理也无法使用.那么除了静态加载的那部分内存,真正需要管理的内存可以分为两种: 数据都在栈上, 部分数据在堆上部分数据在栈上.比如i32都在栈上, string则部分在堆上部分在栈上. 对于数据都在栈上的内存对象,我们可以实现copy Trait,这样用起来很方便.类似其他语言的值拷贝.在传递的时候,内存对象会拷贝一份.标准提供的很多基本类型都实现了copy Trait,比如i32, usize, &str 当然自定义的数据结构,比如结构体,你也可以不实现copy Trait,那么这里就牵扯到内存对象所有权move的问题.无论内存对象是仅在栈上还是混合的,在转移对象所有权时,栈上的内容是完整复制过去的,指向堆的指针也会复制过去.同时,旧的栈对象无法再使用. 实现Copy Trait的对象,不能实现Drop Trait; 在内存对象超出其作用域时,会自动调用其Drop Trait.当然rust为了保留完整的功能,也通过mem::ManuallyDrop提供了不受限的内存. 这里也能看出rust内存管理的一些设计理念,在够用的情况下,尽量把内存管理交给rust编译器去检查; 在需要更强的扩展时,通过留的小口子获得功能增强; 在审视安全问题时,需要check的代码就非常少. 2. 每个借用都不能超出其引用对象的作用域范围 这里还有另一个问题,有一些比较大的内存对象,我们不希望经常拷贝来拷贝去,那么就需要实现类似引用的功能. rust为了避免悬垂指针,就引入了生命周期的概念. 每个对象和每个借用都有其生命周期标注. 在大多数情况下,该标注都是编译器自动添加和检查的. 但是还是有部分场景是编译器无法在编译期确定的,这就需要开发者手动添加生命周期标注,来指明各借用及其对象间的关系. 编译器则会在函数调用方和实现方两侧进行检查,只要能通过检查,至少是内存安全的. 为什么需要生命周期标注? 我想可能还有种原因是为了编译的速度,rust是按各函数单元编译的.因此无法按照调用链做全局分析,所以有些从上下文很容易看出来的生命周期标注,rust依然需要开发者去标注. 在标注的时候,还是要牢记: 可读不可写,可写不可读.可变引用有且只能有一个; 关于生命周期这块发现个不错的帖子: https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md/

    作者回复: 非常棒!

    2021-10-13
    2
    25
  • pedro
    Result<String, ()> 占用多少内存?为什么? 还是 24,也就是说 () 压根不占内存,至于为什么,猜测应该是编译器优化,避免了内存浪费。

    作者回复: 对!首先 () 的确不占内存。然后在文中我也提到,Rust 编译器会做一些优化: > Rust 是这么处理的,我们知道,引用类型的第一个域是个指针,而指针是不可能等于 0 的,但是我们可以复用这个指针:当其为 0 时,表示 None,否则是 Some。 对于 Result<String, ()> 也是如此,String 第一个域是指针,而指针不能为空,所以当它为空的时候,正好可以表述 Err(())。

    2021-09-15
    5
    17
  • Marvichov
    1. align和padding不应该和bus size有关吗? 如果32bit机器, struct的起始地址需要是4的倍数嘛? 还是说随便从哪里开始都可以? align为啥不是4的倍数呢? wiki: The CPU in modern computer hardware performs reads and writes to memory most efficiently when the data is **naturally** aligned, which generally means that the data's memory address is a multiple of the data size. For instance, in a 32-bit architecture, the data may be aligned if the data is stored in four consecutive bytes and the first byte lies on a 4-byte boundary. fn main() { // 4, 4 println!("sizeof S1: {}, S2: {}", size_of::<S1>(), size_of::<S2>()); // 2, 2 println!("alignof S1: {}, S2: {}", align_of::<S1>(), align_of::<S2>()); // 4, 8 println!( "alignof i32: {}, i64: {}", align_of::<i32>(), align_of::<i64>() ); } 2. 为啥不是3的倍数呢? struct S2 { c: [u8; 3], b: u16, } // align_of<S2> is 2 3. rust能自动帮人reorder memory layout, 会不会导致struct的abi不稳定?

    作者回复: 1. 你引用的文字写的很清楚:which generally means that the data's memory address is a multiple of the *data size*。你想想一个 struct A {a: u8},它的 data size 是多少?如果要把它 align 在 64bit 上,那所有的 network buffer (相当于 Vec<u8>)都完蛋了,需要膨胀 8 倍。 这里有段代码,你可以看看,思考一下每个打印地址都如何对齐,然后运行感受一下: ```rust #[derive(Default)] struct Align1 { a: u8, b: usize, c: u32 } #[derive(Default)] struct Align2 { a: u8, } fn main() { let s1 = "a"; let s2 = "aaaa"; let s3 = "hello"; let a = Align1::default(); let b = Align2::default(); println!("{:p}", s1); println!("{:p}", s2); println!("{:p}", s3); println!("Align1.a: {:p}", &a.a); println!("Align1.b: {:p}", &a.b); println!("Align1.c: {:p}", &a.c); println!("Align2.a: {:p}", &b.a); } ``` playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9ab8c85b7065fb0374b046548517821f 2. c 对齐是 1,b 对齐是 2,所以 s2 是 2。注意 align 是对齐,不是长度,切记。就像 [u8; 1024] 长度是 1024,对齐依旧是 1。 3. Rust 目前不对外提供稳定的 ABI。所以如果要想以二进制形式分发,需要提供 C ABI 的接口。比如 struct 需要使用 #[repr(c)],很多标准库的数据结构需要使用对应的 C 结构(如 String)。所有的泛型函数,trait 方法,struct 方法,都需要对应的 C 函数封装。

    2021-09-15
    5
    10
  • 罗杰
    对于 Result<T, io::Error> 这一列的值不是特别理解,老师可能解释一下吗?

    作者回复: Result<T, E> 需要提供一个 E 类型代表错误,而在 show_size! 宏中,我们只传入了 T 的类型,所以这里就随便把 E 写死成 std::io::Error 了。std::io::Error 是 16 个字节,所以 Result<T, E>,如果不能优化的话,要么是 T + 8 个字节(T > 16),要么是 24 个字节(16 + 8)。

    2021-09-17
    2
    6
  • Michael
    老师,想知道 rust 中的 feature 是干什么用的,怎么开发?现在能看到经常有标准库中的: #[stable(feature = "rust1", since = "1.0.0")] 或者 Cargo.toml中的 tokio = { version = "1", features = ["full"] } 这些都是什么意思?怎么自己定义

    作者回复: feature 用作条件编译,你可以根据需要选择使用库的某些 feature。它的好处是可以让编译出的二进制比较灵活,根据需要装入不同的功能。在 docs.rs 下的某个库的文档中,你可以看到它都有哪些 feature。 定义 feature,你可以看 cargo book:https://doc.rust-lang.org/cargo/reference/features.html。下面是一个简单的例子: 在 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 编译

    2021-09-16
    2
    4
  • Christian
    三个字长➕一个字节,这种情况下这个字节可能会被优化掉,原理同 Option。

    作者回复: 正确!

    2021-09-15
    3
  • 0@1
    老师下面的代码,A没实现Drop, 编译器是不是会给他生成个默认的实现,如果不是,那编译器内部是不是有另外一种类似的机制比如xx callback,当某个结构体生命结束时,都会调用,只不过不向开发者开放? struct A(B); struct B; impl Drop for B { fn drop(&mut self) { println!("B droped"); } }

    作者回复: A 不需要显示实现 Drop。文中已经说过编译器会依次为数据结构的每个字段调用其 Drop(如果有 Drop 实现的话)。你可以认为当一个需要 drop 的值 a 退出作用域时,都会进行类似 drop(a) 的操作。 你可以看这个代码:https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018。把 playground 切到 Show MIR 再运行看插入的 drop。

    2021-09-17
    2
    2
  • overheat
    正文中第一次提到cheats.rs的时候,写成了cheat.rs。

    编辑回复: 啊确实是,已经修改过来啦,感谢反馈~

    2021-10-17
    1
  • newzai
    C 采用了未定义的方式,由开发者手工控制;C++ 在 C 的基础上改进,引入智能指针,半手工半自动。随后 Java 和 DotNet 使用 GC 对堆内存全面接管,堆内存进入了受控(managed)时代。所谓受控代码(managed code),就是代码在一个“运行时”下工作,由运行时来保证堆内存的安全访问。 这段话描述错误,C++智能指针在11版本引入,在之前虽然有tr版本,那也是2008年前后的事情,而java,C#一开局就引入了GC,javaC#怎么就变成了在C++之后呢?

    作者回复: 嗯。这段时间关系不对,应该把「随后」二字去掉。

    2021-09-19
    2
    1
  • Kerry
    println!("sizeof Result<String, ()>: {}", size_of::<Result<String, ()>>()); sizeof Result<String, ()>: 24 优化思路应该是跟Option<T>类似。Result<String, ()>的false case是(),就相当于是Option<String>,可以用String里的ptr的值来实现零成本抽象?

    作者回复: 对!

    2021-09-16
    1
收起评论
显示
设置
留言
30
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部