You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4.9 KiB

Box<T>堆对象分配

关于作者帅不帅,估计争议还挺多的,但是如果说Box<T>是不是Rust中最常见的智能指针那估计没有任何争议。因为Box<T>允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。

之前我们在所有权章节简单讲过堆栈的概念,这里再补充一些。

Rust中的堆栈

高级语言Python/Java等往往会弱化堆栈的概念但是要用好C/C++/Rust就必须对堆栈有深入的了解原因是两者的内存管理方式不同: 前者有GC垃圾回收机制 因此无需你去关心内存的细节。

栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说操作系统对栈内存的大小都有限制因此C语言中无法创建任意长度的数组。在Rust中, main线程的栈大小是8MB,普通线程是2MB然后在函数调用时会在其中创建一个临时栈空间调用结束后Rust会让这个栈空间里的对象自动进入Drop流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。

与栈相反,堆上内存则是从低位地址向上增长,堆内存通常只受物理内存限制,而且通常是不连续的, 因此从性能的角度看,栈往往比对堆更高。

相比其它语言Rust堆上对象还有一个特殊之处它们都拥有一个所有者因此受所有权规则的限制当赋值时发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可) 例如以下代码:

fn main() {
    let b = foo("world");
    println!("{}", b);
}

fn foo(x: &str) -> String {
    let a = "Hello, ".to_string() + x;
    a
}

foo函数中,aString类型,它其实是一个智能指针结构体,该智能指针存储在函数栈中,指向堆上的字符串数据。当被从foo函数转移给main中的b变量时,栈上的智能指针被复制一份赋予给b,而底层数据无需发生改变,这样就完成了所有权从foo函数内部到b的转移.

堆栈的性能

很多人可能会觉得栈的性能肯定比堆高,其实未必。 由于我们在后面的性能专题会专门讲解堆栈的性能问题,因此这里就大概给出结论:

  • 小型数据,在栈上的分配性能和读取性能都要比堆上高
  • 中型数据栈上分配性能高但是读取性能和堆上并无区别因为无法利用寄存器或CPU高速缓存最终还是要经过一次内存寻址
  • 大型数据,只建议在堆上分配和使用

总之栈的分配速度肯定比堆上快但是读取速度往往取决于你的数据能不能放入寄存器或CPU高速缓存。 因此不要仅仅因为堆上性能不如栈这个印象,就总是优先选择栈,导致代码更复杂的实现。

Box的使用场景

由于Box是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此Box相比其它智能指针,功能较为单一,可以在以下场景中使用它:

  • 特意的将数据分配在堆上
  • 数据较大时,又不想在转移所有权时进行数据拷贝
  • 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时
  • 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型

以上场景,我们在本章将一一讲解,后面车速较快,请系好安全带。

使用Box<T>将数据存储在堆上

如果一个变量拥有一个数值let a = 3, 那变量a必然是存储在栈上的,那如果我们想要a的值存储在堆上就需要使用Boxt<T>:

fn main() {
    let a = Box::new(3);
    println!("a = {}", a); // a = 3
    
    // 下面一行代码将报错
    // let b = a + 1; // cannot add `{integer}` to `Box<{integer}>`
}

这样就可以创建一个智能指针指向了存储在堆上的5,并且a持有了该指针。在本章的引言中,我们提到了智能指针往往都实现了DerefDrop特征,因此:

  • println!可以正常打印出a的值,是因为它隐式的调用了Deref对智能指针a进行了解引用
  • 最后一行代码let b = a + 1报错,是因为在表达式中,我们无法自动隐式的执行Deref解引用操作, 你需要使用*操作符let b = *a + 1,来显式的进行解引用
  • a持有的智能指针将在作用结束(main函数结束)时,被释放掉,这是因为Box<T>实现了Drop特征

以上的例子在实际代码中其实很少会存在因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上由于寄存器、CPU缓存的原因它的性能将更好而且代码可读性也更好。

Box::leak