11|内存管理:从创建到消亡,值都经历了什么?
该思维导图由 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-13225 - pedroResult<String, ()> 占用多少内存?为什么? 还是 24,也就是说 () 压根不占内存,至于为什么,猜测应该是编译器优化,避免了内存浪费。
作者回复: 对!首先 () 的确不占内存。然后在文中我也提到,Rust 编译器会做一些优化: > Rust 是这么处理的,我们知道,引用类型的第一个域是个指针,而指针是不可能等于 0 的,但是我们可以复用这个指针:当其为 0 时,表示 None,否则是 Some。 对于 Result<String, ()> 也是如此,String 第一个域是指针,而指针不能为空,所以当它为空的时候,正好可以表述 Err(())。
2021-09-15517 - Marvichov1. 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-15510 - 罗杰对于 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-1726 - 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-1624 - Christian三个字长➕一个字节,这种情况下这个字节可能会被优化掉,原理同 Option。
作者回复: 正确!
2021-09-153 - 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-1722 - overheat正文中第一次提到cheats.rs的时候,写成了cheat.rs。
编辑回复: 啊确实是,已经修改过来啦,感谢反馈~
2021-10-171 - newzaiC 采用了未定义的方式,由开发者手工控制;C++ 在 C 的基础上改进,引入智能指针,半手工半自动。随后 Java 和 DotNet 使用 GC 对堆内存全面接管,堆内存进入了受控(managed)时代。所谓受控代码(managed code),就是代码在一个“运行时”下工作,由运行时来保证堆内存的安全访问。 这段话描述错误,C++智能指针在11版本引入,在之前虽然有tr版本,那也是2008年前后的事情,而java,C#一开局就引入了GC,javaC#怎么就变成了在C++之后呢?
作者回复: 嗯。这段时间关系不对,应该把「随后」二字去掉。
2021-09-1921 - Kerryprintln!("sizeof Result<String, ()>: {}", size_of::<Result<String, ()>>()); sizeof Result<String, ()>: 24 优化思路应该是跟Option<T>类似。Result<String, ()>的false case是(),就相当于是Option<String>,可以用String里的ptr的值来实现零成本抽象?
作者回复: 对!
2021-09-161