rust多个属性宏叠加_学一点 Rust 内存模型会发生什么呢(1)

大家好,我又开了一个新的系列,这次来讲讲 Rust 的内存模型。实际上 Rust 的内存模型总体并没有最终定稿。但是我可以试着讲讲这里比较简单的,被大家公认的部分。比较难的部分我到现在也不太会,等着官方提供了更好的材料再说。(为挖坑不填埋下了伏笔)

言归正传,让我们先从位置(place)与值(value)讲起。

位置

Rust里的位置与值大致对应着其他语言的“左值”和“右值”的概念,但是Rust的要更“弹性”一点。什么是位置和值呢? 我来打个比方,比如你用铅笔在一张白纸上写下42这个数字。那么这张白纸就是“位置”,因为它可以写字。而42就是这个数值。 如果你在计算器上按出了42这个数字,那么计算器也是一个“位置”,里面可以放42这个值。

概念上,一个位置的状态总是处于五种状态之中。
1. 还没有初始化(一张白纸、计算器刚开机)。
2. 被初始化了,里面有一个值(写好了42的白纸)。
3. 被初始化了,但是里面的值被移走了(写好了42的白纸,上面打了个叉,表示42不在这里了)。
4. 被初始化了,里面有一个值,这个值正在被共享借用。
5. 被初始化了,里面有一个值,这个值正在被独占借用。

这里的4和5两种状态是Rust的特色,后面会再详细说。

还要提一句,对编译器来说由于判断、循环等逻辑跳转的存在,有时候某个位置的静态分析结论会有“要么是状态A,要么是状态B”的情况。在语言里,针对这些“叠加态”是有专门特殊的规则的,在这里也先不细说。

除了状态这个属性之外,位置还有两种其他的属性。

位置的类型属性。类型首先描述了这个位置的大小(size),内存对齐(align),析构行为(dtor)。此外类型还与Rust独特的特质(trait)系统紧密集成,特别是标记特质(marker trait),影响最大的比如Copy, Send, Sync等等。比如,如果这个类型是满足Copy特质的,那么上面的状态3就不存在——位置里面的值无法被移走,换句话说,不存在存储位置存在但是里面的值已经死亡的情况。

位置的可变性属性。如果位置未被标记为可变(mut),那么上面的状态5就不存在——位置里面的值无法被独占借用;同时这个位置没有办法在离开状态1之后重新对它赋值,也就是在状态2、3下,对它执行赋值会编译错误。(状态4本来就没有办法对它执行赋值)这也就断绝了从状态3回到状态2的可能性。如果标记成了可变(mut),就没有上面的这些限制。

好了,那么这里说的位置是从哪来的呢?

首先最最常见的就是变量声明。通过let语句就可以建立新的带名字的位置,也就是变量。变量其实就是名字与这个位置之间的绑定关系。另外static条目会声明全局位置,相应的名字也存在绑定关系。

除了变量以外,函数调用的参数啊、返回值啊,其实都是有对应的位置的。

还有“临时位置”,比如你表达式计算1+2+4=7的时候,有三个临时位置存1、2、4这三个数,此外还有一个临时位置临时存了一下1+2的结果,3这个数。临时位置通常是隐式标记成了可变(mut)的。

这还没完,在数组(array)、元组(tuple)、结构体(struct)、枚举体(enum)、联合体(union)、闭包、生成器之类的类型的值的内部也会存在一些小的位置。走过一条访问路径就能访问这些值内部的小的位置,虽然语法、限制要求各不相同,但是本质都是相似的。

简单说了一下位置,我们再来说值(value)。我们常说的数值、文本等等种种,乃至对某个位置的引用之类的,都是值。

值有类型,也有唯一的内存表示,内存表示的长度、对齐都是由类型决定的。

由于在编译时就已经对所有关于位置的逻辑做好了事先的规划,所以运行时在通常情况下是不需要再来当场规划的。这一点也是Rust与Python之类的语言的本质区别点。Python之类的语言的运行时的一个很重要的功能,就是作为值的运行时管理器。而Rust在编译时刻做好了规划,在运行时值只剩下内存表示,存储于预先规划好的位置之内。通常来说,在Rust程序运行时,值的数量是不可数的。(而Python虚拟机、JVM里对象的数量是可数甚至可遍历的)

另一个有趣但是经常被忽视的事情是:const 条目定义的常数、fn 条目定义的函数,这些本身都是只有常数值,没有位置的。但是当你每次使用这些常数值的时候,会当场创建一个临时的位置。刚才说过,临时位置是可变的,所以你可以在这时引用、修改这个临时位置里的值,而不会影响原始的常数值。

存储区

在某些编程语言中,类型被分类并采取不同的存储策略。首先先说结论:在Rust中,存储策略是上文说的位置的特性,而与类型完全无关

存储区的分类

对于通常的系统编程来说,常见的存储区大致有三类:全局存储区、调用堆栈存储区(栈)、堆存储区(堆)。但是这是一种惯例而不是本质要求。Rust 作为一门编程语言,语言本身不要求这些概念,而在标准库对常用操作系统的适配中,存在着这样的对应关系。

在现实情况,特别是一些极端情况(如嵌入式编程等等),存储区的划分是可以变通的。比如如果存储比较紧张,首先舍弃的就是堆存储区。在这样的情况下,如果有必要,可以配置成由全局存储区定制提供(概念上是)堆存储区的存储空间;如果没必要,则自始至终不使用(概念上是)堆存储区的机制。其次,调用堆栈存储区一般都是会准备的,但是在极端情况下也可以舍弃,这时程序需要编译成一个整体,而不能存在函数之间的相互调用,更不能支持递归函数。

Rust 的存储策略

一般而言,rust程序的static条目所提供的位置是来自全局存储区的。函数中变量提供的位置是来自调用堆栈存储区的,临时位置大部分也是如此,但是在某些情况下对某些常量提升了的值,移至全局存储区也是存在可能性的。async 函数中的变量、闭包的捕获等等位置,与结构体的字段等等,由于都是类型系统刻画的更大的值中的局部位置,所以不需要对应存储区(它们对应更大的位置中的一小块)。

上面的这段话里完全没有提到堆存储区,那么堆存储区是如何使用的呢?在Rust里有一个全局分配器机制(global allocator),它通常被适配为从操作系统规定的进程的堆存储区分配、释放内存。而某些类型的API设计与全局分配器机制紧密结合——以Box为例,Box<i32> 在构建时通过全局分配器获取出一块合适的堆存储区内存,形成了一个能存放i32类型值的外部位置,并立刻把一个值放进去(状态2)。它的内部状态记录的就是这个外部位置,而它的析构行为则被设定为将这个外部位置里的值析构(进入状态3)、然后通过全局分配器归还这个外部位置对应的空间。在通常的配置下,这个外部位置在堆上,从而允许你把一个i32类型的值通过放进这个位置里来放在堆上;这与整个Box<i32>类型的值和对应的位置是完全无关的

指针与内存地址

在过去的几十年里,C语言在系统编程领域具有很大的影响力。Rust语言在设计时,自然也提供了一定程度的对C语言的兼容性。首先,对位置的引用能够转化为原生指针(mut T 和 const T),然后这两个指针类型又能够转化为usize数值——机器字长的整数。对应于C语言里的指针和内存地址。

可能对某些人有点反直觉的事实:上文我们说到的位置,并不总是需要在运行时对应实际的内存区域,而只是概念上的。代码生成过程、优化过程中,大部分位置会被抹除。代码生成及优化的步骤本身是一个充满了启发式(heuristic)策略的过程,除了基本的原则会被保证之外,做法和结果都是来源于“尽力而为”。这里的基本原则就是:优化后的结果执行起来仿佛跟没有优化过一样。

即使是留下来的那些位置,在概念上,如果编译器能够证明两个位置的使用在时序上没有交集,它甚至可以决定让它们实际对应的存储空间重叠,换句话说,内存地址可以不唯一。

有趣的是,当你试着取某个地址的引用,转化为指针,并打印出来的时候。实际上你会改变编译器的判断。原本可以对应CPU寄存器的位置,编译器会觉得因为这个位置被你获取了内存地址。所以必须在内存里留出对应的位置。算是一种奇妙的观测交互吧。

小结

以上就是我们第一篇的内容啦!欢迎讨论。

第二期传送门:学一点Rust内存模型会发生什么呢(2)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值