请教一个 Rust 数据是在堆上,还是在栈上的问题

2020-05-04 09:04:35 +08:00
 Kaiv2

《 Rust 程序语言设计》

使用Box<T>指向堆上的数据

最简单直接的智能指针是 box,其类型是 Box<T>。box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果

堆栈

栈( Stack )与堆( Heap )
在很多语言中,你并不需要经常考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。我们会在本章的稍后部分描述所有权与栈和堆相关的内容,所以这里只是一个用来预热的简要解释。

栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 后进先出( last in, first out )。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 进栈( pushing onto the stack ),而移出数据叫做 出栈( popping off the stack )。

栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针( pointer )。这个过程称作 在堆上分配内存( allocating on the heap ),有时简称为 “分配”( allocating )。将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。

想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。

入栈比在堆上分配内存要快,因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。

访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。

当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的存在就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。

数据什么时候再堆上,什么时候再栈上呢?

    struct Aa {
        id: i32,
    }
    impl Drop for Aa {
        fn drop(&mut self) {
            println!("Aa Drop, id: {}", self.id);
        }
    }

    #[test]
    fn test_1() {
        let a1 = Aa { id: 1 }; // 数据分配在栈中
        let a1 = Rc::new(a1); // 数据 move 到了堆中?
    }
    
    #[test]
    fn test_2() {
        let a1 = Aa { id: 1 }; // 数据分配在栈中
        let a1 = Box::new(a1); // 数据 move 到了堆中?
    }

对这个理解也是比较模糊,不知道上面的代码理解的对不对,哪位大神可以深入浅出的解释一些呢?感谢❤

4796 次点击
所在节点    Rust
8 条回复
secondwtq
2020-05-04 11:18:15 +08:00
这说的很清楚了啊:
“box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针”

楼主学过 C++ 没?
somalia
2020-05-04 11:33:19 +08:00
智能指针!
Kaiv2
2020-05-04 12:29:35 +08:00
@secondwtq 学过一点 c/c++,可能我没表述清除。如上面代码,有种情况是我先初始化值在栈上然后 Box::new()的情况。这是是否把栈上的数据移动到了堆上呢?
jmc891205
2020-05-04 12:53:11 +08:00
最简单的理解就是分配在栈上的内存作用域就是当前函数,当前函数退出了就要销毁;而分配在堆上的内存可以在程序全局里使用,某些语言要开发者手动释放,某些语言是由 GC 负责释放。
你的两个 test 里第二个 a1 都是把数据从栈上 move 到了堆上没错。
Senevan
2020-05-04 18:36:41 +08:00
第二段代码 Rc::new(RefCell::new(A { name: String::from("zhang"), })); 的参数是一个 rvalue struct expression 没有在栈上分配
Kaiv2
2020-05-04 19:50:00 +08:00
@Senevan 这里确实是的。
应该使用第一段代码的写法的
let a = A {
name: String::from("zhang"),
};
let a = Rc::new(RefCell::new(a));
secondwtq
2020-05-04 22:06:53 +08:00
你学过 C++ 应该民白 move 做了什么(只是 C++ 里面被 move 过后的值是 unspecified 的,Rust 是直接不能用,另外 C 基础没啥用)
底层是 copy 还是什么并不重要,底层也并没有“所有权”之类的概念,编译器在保持行为不变的前提下可以随便怎么优化
实在好奇,开 godbolt 看下就是了

@Senevan “rvalue struct expression 没有在栈上分配” source?
Kaiv2
2020-05-05 08:18:10 +08:00
@secondwtq 谢谢,大致的明白了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/668386

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX