V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Kaiv2
V2EX  ›  Rust

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

  •  
  •   Kaiv2 · 2020-05-04 09:04:35 +08:00 · 4761 次点击
    这是一个创建于 1666 天前的主题,其中的信息可能已经有所发展或是发生改变。

    《 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 到了堆中?
        }
    

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

    第 1 条附言  ·  2020-05-04 14:18:50 +08:00

    标题可能写的不正确,应该是"数据什么时候放到堆中了"

    生命周期与引用有效性

    写了段代码加深了理解,普通引用与智能指针,智能指针 Box<T>可以拥有值的所有权共享到其他地方(堆中分配空间),普通引用只是对所有权的借用,当离开作用域了,值就销毁了无法共享数据(这里理解为在栈上分配了空间),当然 Box<T>离开了作用域也是销毁了数据。但是,如果需要多个地方共享同一份数据普通引用是无法做到的,除非是在栈上复制来复制去的,那这也不是共享同一份数据了。

    // 场景 多个 C 对象需要共享  A, B 数据,对A需要有修改权限
    use std::cell::RefCell;
    use std::rc::Rc;
    #[derive(Debug)]
    struct A {
        name: String,
    }
    impl Drop for A {
        fn drop(&mut self) {
            println!("A drop: {}", self.name);
        }
    }
    
    impl Drop for B {
        fn drop(&mut self) {
            println!("B drop: {}", self.name);
        }
    }
    #[derive(Debug)]
    struct B {
        name: String,
    }
    #[derive(Debug)]
    struct C {
        list_a: Vec<Rc<RefCell<A>>>,
        list_b: Vec<Rc<B>>,
    }
    
    #[test]
    fn test_rc() {
        let mut c = C {
            list_a: Vec::new(),
            list_b: Vec::new(),
        };
        let a = Rc::new(RefCell::new(A {
            name: String::from("zhang"),
        }));
        let b = Rc::new(B {
            name: String::from("wu"),
        });
        println!("a ptr: {:p}", a);
        println!("b ptr: {:p}", b);
        c.list_a.push(a);
        c.list_b.push(b);
        f2(&c);
        c.list_b.push(Rc::new(B {
            name: String::from("li"),
        }));
        println!("{:?}", c);
    }
    
    fn f2(c: &C) {
        let mut c2 = C {
            list_a: Vec::new(),
            list_b: Vec::new(),
        };
    
        for c1a in c.list_a.iter() {
            println!("a ptr: {:p}", *c1a);
            let mut a = (*c1a).borrow_mut();
            a.name = String::from("我改了");
            c2.list_a.push(Rc::clone(c1a));
        }
        for c1b in c.list_b.iter() {
            println!("b ptr: {:p}", *c1b);
            c2.list_b.push(Rc::clone(c1b));
        }
        println!("{:?}", c2);
    }
    
    

    问题点在这里我在栈上分配了 A 然后使用了Rc<T>共享到多个地方,这里是否没有在栈中分配空间,直接在堆中分配空间了?

    • 如果说是在栈上分配了空间,那应该是copy到堆了吧?写到这里想到应该是没有理解" move 语义做了什么"?个人理解应该是直接在堆上分配了空间。在调用Rc::new()时所有权已经到了Rc<T>中,无法再使用原来的变量了。怎么还是感觉哪里怪怪的(是不是编译器偷偷做了啥)。。。
    8 条回复    2020-05-05 08:18:10 +08:00
    secondwtq
        1
    secondwtq  
       2020-05-04 11:18:15 +08:00 via iPhone
    这说的很清楚了啊:
    “box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针”

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

    @Senevan “rvalue struct expression 没有在栈上分配” source?
    Kaiv2
        8
    Kaiv2  
    OP
       2020-05-05 08:18:10 +08:00
    @secondwtq 谢谢,大致的明白了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1169 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 18:41 · PVG 02:41 · LAX 10:41 · JFK 13:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.