Rust如何使用堆和栈

有些数据适合存放于堆,有些数据适合存放于栈。

(1).栈适合存放存活时间短的数据

比如函数内部的局部变量适合存放在栈中,因为函数返回后,该函数中声明的局部变量就没有意义了,随着函数栈帧的释放,该栈中的所有数据也随之消失。

与之对应的,存活时间长的数据通常应该存放在堆空间中。比如多个函数(有不同栈帧)共用的数据应该存放在堆中,这样即使一个函数返回也不会销毁这份数据。

(2).数据要存放于栈中,要求数据所属数据类型的大小是已知的。因为只有这样,Rust编译器才知道在栈中为该数据分配多少内存。

与之对应的,如果无法在编译期间得知数据类型的大小,该数据将不允许存放在栈中,只能存放在堆中。

例如,i32类型的数据存放在栈中,因为i32类型的大小是固定的,无论对它做什么操作,只要它仍然是i32类型,那么它的大小就一定是4字节。而String类型的数据是存放在堆中的,因为String类型的字符串是可变而非固定大小的,最初初始化的时候可能是空字符串,但可以在后期向此空字符串中加入任意长度的字符串,编译器显然无法在编译期间就得知字符串的长度。

(3).使用栈的效率要高于使用堆

将数据存放于栈中时,因为编译器已经知道将要存放于栈中数据的大小,所以编译器总是在栈帧中分配合适大小的内存来存放数据。另一方面,栈中数据的存放方式是后进先出。这相当于编译器总是找好各种大小合适的盒子来存放数据并将盒子放在栈的顶部,而释放栈中数据的方式则是从栈顶拿走盒子

与之对应的是将数据存放于堆中时,当程序运行时会向操作系统申请一片空闲的堆内存空间,然后将数据存放进去。但是堆内存空间是无人管理的自由内存区,操作系统想要从堆中找到空闲空间需要做一些额外操作。更严重的是堆中有大量碎片内存的情况,操作系统可能会将多份小的碎片空闲内存通过链表的方式连接起来组成一个大的空闲空间分配给程序,这样的效率是非常低的。

对比堆和栈的使用方式,显然以【盒子】为操作单位且总是跟踪栈顶的栈内存管理方式的效率要远高于堆

其实,可以将栈理解为将物品放进大小合适的纸箱并将纸箱按规律放进储物间,将堆理解为在储物间随便找一个空位置来放置物品。显然,以纸箱为单位来存取物品的效率要高的多,而直接将物品放进凌乱的储物间的效率要低的多,而且储物间随意堆放的东西越多,空闲位置就越零碎,存取物品的效率就越低,且空间利用率就越低。

用一张图来描述它们:

(4).Rust将哪些数据存放于栈中

数据要存放在栈中,要求其数据类型的大小已知。但数据类型大小已知的数据并不一定存放在栈中。

Rust中,有如下类型的数据存放在栈中:

  • 裸指针(一个机器字长)、普通引用(一个机器字长)、胖指针(除了指针外还包含其他元数据信息)
  • 布尔值
  • char
  • 各种整数、浮点数
  • 数组(Rust数组的元素数据类型和数组长度都是固定不变的)
  • 元组

以上分类需要注意几点:

  • 将栈中数据赋值给变量时,数据直接存放在栈中。比如i32类型的33,33直接存放在栈内,而不是在堆中存放33并在栈中存放指向33的引用
  • 引用、胖指针包含了很多种可能:
    • 对于let s = String::from("hello"),字符串数据存放在堆中,但在栈中存放了指向该字符串的胖指针
    • 因此,将堆中数据赋值给变量时,总是将指向该堆数据的指针保存在栈中
    • 切片类型的引用(如&str)是胖指针,存放在栈中
    • 函数指针(fn类型)存放在栈中
  • 有些数据是0字节的,不需要占用空间,比如()
  • 尽管数组和元组是可以存放任意数据的容器,但保存在数组、元组中的要么是栈中值,要么是指向堆中数据的引用,所以这两种类型的值也在栈中

(5).Rust除了使用堆栈,还使用全局内存区(静态变量区和字面量区)

Rust编译器会将全局内存区的数据直接嵌入在二进制程序文件中,当启动并加载程序时,嵌入在全局内存区的数据被放入内存的某个位置。

全局内存区的数据是编译期间就可确定的,且存活于整个程序运行期间。

字符串字面量、static定义的静态变量(相当于全局变量)都会硬编码嵌入到二进制程序的全局内存区

例如:

fn main(){
  let _s = "hello";     // (1)
  let _ss = String::from("hello"); // (2)
  let _arr = ["hello";3];    // (3)
  let _tuple = ("hello",);   // (4)
  // ...
}

上面代码中的几个变量都使用了字符串字面量,且使用的都是相同的字面量"hello",在编译期间,它们会共用同一个"hello",该"hello"会硬编码到二进制程序文件中。当程序被加载到内存时,该被放入到全局内存区,它在全局内存区有自己的内存地址,当运行到以上各行代码时:

  • 代码(1)、(3)、(4),将根据地址取得其引用,并分别保存到变量_s_arr各元素、_tuple元素中
  • 代码(2),将根据地址取得数据并将其拷贝到堆中

(6).Rust中允许使用const定义常量。常量将在编译期间直接以硬编码的方式内联(inline)插入到使用常量的地方

所谓内联,即将它代表的值直接替换到使用它的地方。

比如,定义了常量ABC=33,在第100行和第300行处都使用了常量ABC,那么在编译期间,会将33硬编码到第100行和第300行处。

Rust中,除了const定义的常量会被内联,某些函数也可以被内联。将函数进行内联,表示将该函数对应的代码体直接展开并插入到调用该函数的地方,这样就没有函数调用的开销(比如没有调用函数时申请栈帧、在寄存器保存某些变量等的行为),效率会更高一些。但只有那些频繁调用的短函数才适合被内联,并且内联会导致程序的代码膨胀。