用了这么多年Rust终于搞明白了内存分布!

这篇具有很好参考价值的文章主要介绍了用了这么多年Rust终于搞明白了内存分布!。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

用了这么多年Rust终于搞明白了内存分布!

导读
Rust作为一门学习曲线十分陡峭的语言,掌握其核心基础数据结构的内存分布对学习Rust会有很大的帮助,本文由浅入深仔细介绍了Rust的各个数据结构在内存中的分布情况。
Rust作为一门学习曲线十分陡峭的语言,掌握其核心基础数据结构的内存分布对学习Rust会有很大的帮助,即使对于已经熟悉Rust的同学,深入数据结构分布也能帮助到调优Rust程序。
接下来,我会由浅入深仔细介绍Rust的各个数据结构在内存中的分布情况,帮助大家学习Rust。

先决条件 Prerequisite

在开始介绍之前,我们先做这个几个假设,来更好地帮助后续文章的展开。
  1. 我们本文的机器预设是32位的(主要为了画图可以精简一点),所有和位相关的数据结构均会用上标标记(即这些数据结构占用的是1 machine word)。例如:

  2. 数据结构基本单位示图:

    用了这么多年Rust终于搞明白了内存分布!

蓝色的框框代表1个byte,绿色的框框pointer下的(1|2|3|4)代表pointer在32位机器上rust是4个byte,他们整体被框在绿色框框中代表一个pointer。

 

一、基本类型

不用害怕,让我们把一只小脚试探性地迈入Rust的大门,先看看基础类型的内存分布吧。

用了这么多年Rust终于搞明白了内存分布!

这些数据结构Rust分配的时候都是在栈上的。

 

1.1 Stack栈 vs Heap堆

因为本文会涉及到Rust中栈和堆分配,本小章先来简单讲一下栈和堆。
我们只提炼一些最基本的区别概要,更多的细节可以看这篇文章[1]有比较好的解释。

栈特点:

  1. 分配快
  2. 大小受限
堆特点:
  1. 分配慢
  2. 大小不受限
  3.  

二、元组 Tuple

 

让我们先从比较基础的Rust数据结构Tuple看起。

let a:(char, u8, i32) = ('a', 7, 354);
size_of::<(char, u8, i32)>(); // 打印结果 12
align_of::<(char, u8, i32)>(); // 打印结果 4
该元组由三个元素构成——char、u8和i32,由1 基本类型中可知char占4 bytes,u8占1 byte, i32占4bytes,那么初步计算出来这个tuple占用的总内存应为4+1+4 = 9 bytes。接着,Rust会选择Tuple中对齐值最大的元素为a该元组的对齐值,由此上例alignment是4。有了整体对齐值,Rust会在内存中加入一段填充(padding)来让整体内存占用是alignment的整数倍,本例中加在u8与i32中间是为了保障i32自身的内存对齐。
由于Rust有多种数据排布风格(默认的Rust风格,还有C语言风格,primitive和transparent风格),在Rust风格中,Rust可以对元组中的元素做任意重排,也包括padding的位置,因而图中的排列只是一种可能,也许i32和char的位置在Rust中会进行互换,Rust是根据其优化算法做出其认为最优的排序,对最终排序结果并没有统一规则。

用了这么多年Rust终于搞明白了内存分布!

上图为该tuple的内存分布图。

三、引用 Reference

引用 reference 是Rust中的一个重要概念,相关规则也是支撑了Rust内存安全的重要支柱。我们来看下面的例子。

let a: i8 = 6;
let b : &i8 = &a;

a 是一个i8,b是一个指向 a 的reference,我们可以看下他俩的内存分布。

用了这么多年Rust终于搞明白了内存分布!

首先,Rust会在栈上分配一个大小为1byte的i8存储a,接着会在内存另外一个空间(不一定和a连续)分配b,b中存储的内存空间会指向a所在的内存空间,同时b的内存占用大小即pointer的大小。

需要注意的是,&T和&mut T在内存分布上规则一致,他们的区别是在使用方式和编译器处理方式上。

 

四、Array数租 和 Vector动态数组

接下来我们来看看Rust的数组Array和动态数组Vector的内存分布,以下面的数组和动态数组为例。


let a: [i8; 3] = [1, 2, 3];
let b: Vec<i8> = vec![1, 2, 3];

数组Array是固定大小的,所以在创建的时候都指定好了长度;动态数组Vector,由其名字就可以知道他是可以自由伸缩的,那么我们来看看Rust是怎么在内存上存储这两位数据结构的。

用了这么多年Rust终于搞明白了内存分布!

对于Array a,由于他固定大小为3个i8,Rust即在栈上为其分配了3 * 1 byte个内存。
对于Vector b就有点特殊啦,他会由如下三个部分组成:
  1. pointer : pointer b会指向vector b在堆上的实际数据(目前是1, 2, 3 共3 * 1 byte),

  2. cap(图中上标32代表这个值和机器位数有关,最后复习一次哦): cap代表最多多少个T(本例中T是i8)的内存可以在堆上让这个动态数组使用,默认大小为创建时的T个数,可根据使用需求自动扩容,但每次扩容时会带来reallocate影响到性能。

  3. len (1 machine word),代表目前有多少个T(本例中T是i8)的内存真实被该动态数组使用。

以上即可看到数组和动态数组由于在“动态”这个特点上的不同,出现的内存分布差异啦。

 

4.1 Slice 数组切片

接下来,我们通过Array和Vector来看下Rust中切片的内存分布实现。
假设我们想获取到上面例子中a和b两个Array和Vector的前两个元素。

let slice_1: [i32] = a[0..2];
let slice_2: [i32] = b[0..2];

然而,对于[i32],Rust没法在编译时明确这个变量需要多少内存,因而也没法在栈上分配内存,因而上例中的slice_1和slice_2实际上会编译失败。这样的变量称之为dynamically sized type,后续会讲到string slice和trait object也属于这个范畴。

因而,通常我们使用一个reference来指向一个Slice切片,让我们看下例

let slice_1: &[i32] = &a[0..2]
let slice_2: &[i32] = &b[0..2]

当reference指向dynamically sized type时,Rust实际会使用到一个胖指针(fat pointer),其中包含:

  1. pointer (1 machine word):指向实际被切片的数据。

  2. length (1 machine word): 切片长度,即有多少个T(本例中T为i32)。

我们可以看下上述例子的内存分布图。

用了这么多年Rust终于搞明白了内存分布!

五、String, str, &str

接下来让我们来看下String, str 和&str的内存分布。以一个例子开始吧。

let s1: String = String::from(“HELLO”);
let s2: &str = “ЗдP”;  // д -> Russian Language
let s3: &str = &s1[1..3];

首先,s1是一个String,String实质上就是Vec的一个包装,其中也是在栈上有一个指针 + cap( 1 machine word ) + len ( 1 machine word ),指针指向了该String实际在堆上的值。String是保证UTF-8兼容的。

如果我们直接在变量中存了一个字符串字面值(string literal),例如s2,那么这个变量会是一个指向string slice的指针。这个string数据不会存储在堆heap上,而是会直接存在编译后的二进制中,同时他们具有static生命周期,即直到程序结束前都不会被释放。如同前面讲的slice以后,&str也同样是个胖指针,同时包含了实际数据的内存地址和数据长度(一共2 machine words)。这里的例子里用了一个特殊字符д,由于UTF-8是一种可变长的编码方式,这里可以看到д就用了2个byte来表达。
s3的情况与4.1中类似,使用到一个胖指针(fat pointer),其中包含:
  1. pointer (1 machine word):指向实际被切片的字符串。

  2. length (1 machine word): 切片长度。

用了这么多年Rust终于搞明白了内存分布!

六、Struct

Rust有三种结构体类型定义方式:

 

6.1 unit-like Struct

struct Data

用了这么多年Rust终于搞明白了内存分布!

由于并没有定义Data结构体的细节,Rust也不会为其分配任何内存。

 

6.2 Struct with named fields && tuple-like struct

这两种结构体的内存分配方式是类似的,我们来看一个例子就好。

struct Data {
   nums: Vec<usize>,
   dimension: (usize, usize),
}

用了这么多年Rust终于搞明白了内存分布!

首先,nums是Vec,占用3个 machine word(pointer + cap + len),pointer指向heap上实际动态数组的值;dimension是两个usize组成的tuple,占用2个machine word。由于之前谈到,Rust风格的数据排布是可以做任意重排的,所以具体的padding在图中就并没有画出了。

七、Enum


enum HTTPStatus {
   Ok,
   NotFound,
}

对于C-style enum,在内存中,rust会根据该enum中最大的数来选择内存占用最小的int来存储,此例中没有指定就会默认Ok为0,NotFound为1,Rust选择占用1 byte的i8来存储enum。

同时,每个Enum的整数值是可以指定的,例如:

enum HttpStatus {
   Ok = 200,
  NotFound = 404,
}
本例中,Rust会选择占用2 byte的i16来存储enum(以满足存储404)。
接着我们来看更复杂一些的Enum:

 Empty,
  Number(i32),
  Array(Vec<i32>),
 
对于这类有具体数据结构的Enum,每一个Enum中的元素都有一个 1 byte的tag,tag用于标识属于Enum中具体哪个变量。此例中,Empty的话tag为0,而Empty后的内存空间都是为了满足对齐要求而构造的padding,后续的i32和Vec均和之前介绍的分布一样,在enum中它们有不同的几点:加入了1 byte tag以及padding,因而也可以看到每一个Enum所占的空间由其中占用空间最大的变量所决定,如果要优化Enum的空间占用,可以从削减其中最大元素做起。
(padding的位置是不固定的,Rust会根据具体数据结构的内存分布调整padding位置以做优化)

 

7.1 Option

Rust中的Option实质上是便是一种Enum,我们可以看下Option的定义:

pub enum Option<T> {
  None,
  Some(T),
}
Rust通过None和Some的区分,避免了其他语言中可能发生的空指针访问问题。我们可以看下Option<Box<i32>>这个例子,稍后我们会仔细介绍Box,在这里你可以先理解Box会将原来的i32从栈放到堆,然后Box会是一个指针指向原来的i32新的堆的地址。

用了这么多年Rust终于搞明白了内存分布!

由于pointer本身只占1 machine word,而tag的存在多了1 byte,导致Rust需要根据对齐值加入paddign使其对齐,使得整体内存占用提升,很明显这里有可以优化的空间。于是,Rust对于类似Box这样的不允许为null的SmartPointer,Rust进行了如下优化:

用了这么多年Rust终于搞明白了内存分布!

如此,整体内存占用降到了1 machine word。如果该Option值为0,那么Rust就知道他是None,如果非0,那么Rust就知道他是Some,通过这样省去了tag的作用并且节省了内存空间消耗。

八、Box

对于通常默认分配在栈上的变量,使用 Box 可以将其分配到堆上,栈空间上只用分配指向堆数据的指针。
我们以一个tuple为例let t: (i32, String) = (5, “Hello”.to_string); ,在没有经过Box处理前,它的内存分布如下图:

用了这么多年Rust终于搞明白了内存分布!


(图中省去了padding)
如果我们将该数据结构放到Box b中,即

let t: (i32, String) = (5, “Hello”.to_string);
let mut b = Box::new(t);
内存分布则如下图:

用了这么多年Rust终于搞明白了内存分布!

可以看到,原本在栈上的内容都被转移到Heap上,减少了我们在栈上的内存空间消耗。文章来源地址https://www.toymoban.com/news/detail-433120.html

基础篇在这里就完结啦,后续会继续展开进阶篇,对Rust的Copy&Move, 智能指针, Arc等特性做进一步展开。
参考链接:
[1]https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/the-stack-and-the-heap.html
作者|吴超(律霜)

到了这里,关于用了这么多年Rust终于搞明白了内存分布!的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 干了这么多年C#,后悔没早点用这种“分页”,简单/高效/易维护

    【前言】 干了这么多年C#,后悔没早点用这种“分页”,简单/高效/易维护,比其它的分页方式强多了,不信你自己看。   【正文】 支持.Net Core(2.0及以上)与.Net Framework(4.5及以上) 可以部署在Docker, Windows, Linux, Mac。   从NuGet引入DeveloperSharp包,然后像如下那样使用分页功能:

    2024年02月13日
    浏览(53)
  • 终于有人把大数定律讲明白了

    导读: 在一些情况下,概率是由频率推导而来的,要得到可信的概率,就要大量重复地试验。而且,重复试验的次数越多,结论就越让人信服。那么,为何人们直觉上更愿意相信从大数据中得到的统计结果,而不是从小数据中得到的经验呢? 作者:徐晟 来源:大数据DT(I

    2023年04月14日
    浏览(47)
  • 什么是目标检测?有哪些应用?终于有人讲明白了

    导读: 计算机视觉(Computer Vision,CV)是一门教计算机如何“看”世界的学科。计算机视觉包含多个分支,其中图像分类、目标检测、图像分割、目标跟踪等是计算机视觉领域最重要的研究课题。本文将着重介绍目标检测的相关知识,并提供一些实例,以帮助读者对目标检测

    2023年04月24日
    浏览(47)
  • 云计算发展的 4 个阶段,终于有人讲明白了

    导读: 云计算从诞生至今,经历了四个发展阶段,目前仍然在高速演进中。 作者:阿里云智能-全球技术服务部 来源:大数据DT(ID:hzdashuju) 01 公有云 公有云是云计算最早期的形态,也是截至目前众多云厂商期望实现的终极形态,它是从弹性计算共享资源租用服务开始的。

    2024年02月06日
    浏览(51)
  • 用了这么久rabbitmq,你还不知道它的目录结构吗?

    rabbitmq配置目录:/etc/rabbitmq/ ​ 常见配置文件有: (1)配置文件 rabbitmq.conf (2)环境变量文件 rabbitmq-env.conf (3)补充配置文件 advanced.config rabbitmq数据目录:/var/lib/rabbitmq/ 目录文件有: rabbitmq日志文件: /var/log/rabbitmq ​ 目录文件有: rabbitmq命令脚本:/usr/lib/rabbitmq/ 1.bin目录

    2024年02月16日
    浏览(48)
  • Spring使用三级缓存解决循环依赖?终于完全弄明白了

    文章阅读前推荐 推荐先去看看源码,源码很短,但是对于我们在脑子里构建一个完整思路很重要。看起来非常简单,只需要双击shift,全局查找文件:AbstractAutowireCapableBeanFactory,找到550行左右的doCreateBean方法,重点看一下580行到600行这20行代码就行,包含了三级缓存、属性注

    2024年03月25日
    浏览(69)
  • 云计算与数字化转型的关系,终于有人讲明白了

    导读: 云计算与数字化转型是相辅相成的关系。 作者:阿里云智能-全球技术服务部 来源:大数据DT(ID:hzdashuju) 01 云计算带来的重大变化 通过数据提升效率、降低成本、进行业务创新,这个想法不是第一天出现,在大型机、小型机时代就已经出现了这种观点。那个时候,

    2024年01月25日
    浏览(45)
  • 到底什么是机器学习模型?这篇文章终于讲明白了

    机器学习 (Machine Learning) 是对研究问题进行模型假设,利用计算机从训练数据中学习得到模型参数,并最终对数据进行预测和分析的一门学科。 **模型是机器学习的核心组成要素。**本文从模型的广义概念出发,引申出机器学习模型的基本定义,并就机器学习中容易混淆的概念

    2024年02月05日
    浏览(45)
  • 什么是AB实验?能解决什么问题?终于有人讲明白了

    导读: 走向身边的AB实验。 作者:木羊同学 来源:大数据DT(ID:hzdashuju) “AB实验”是一个从统计学中借来的工具。我和大家一样,每次只要看到“统计学”这三个字,下意识就觉得这事和我没啥关系,然后手就忍不住想要点击下一条文章。不过且慢,开篇我说AB实验是一

    2024年02月10日
    浏览(48)
  • vue开发者vite多环境配置,终于搞明白了

    在看项目的过程中,发现有类似服务端多环境配置的配置,所以研究了下,在网上有多个方案,选了一个当前在用的吧,另外一个没验证 对于使用Vite构建的Vue项目,可以使用Vite提供的环境变量来实现多环境配置。 Vite 使用  dotenv  从  环境文件目录  中加载环境文件,默认

    2024年02月06日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包