栈借用模型,第一部分
大前提
(1)引用及其衍生的所有引用只能在他们的生命周期内使用
(2)在借出的有效期到期之前,借出者不会被使用。
一句话:对借入者引用的使用(及其衍生的所有引用)必须在下一次使用借出者引用之前进行
操作语义
tag(PointerId,指针ID)
区分指向同一个内存区域的引用,每一个引用使用一个id来标识,叫做PointerId,当引用创建的时候,对应一个PointerId
复制引用的时候,就是复制PointerId
Pointer(location,tag) 指针值
Pointer是标量
Pointer由两个部分组成
location,代表指针指向的内存地址
tag标记该指针值是指针类型或者引用类型,但是在运行时,它们都是指针值.
元素(项,Item)
元素是栈的元素
Unique(t)在所有元素中就是唯一带有标签t的元素
原始数据类型
是标量
标量
标量有两种
Pointer(location,tag)
原始数据类型
栈(借用栈)
一组元素,先进后出模型
规则1 new-mutable-ref
任何时候使用&mut表达式从存在的Pointer(location,tag)创建'new-mutable-ref',该操作有两个步骤
1.应用use-1
2.新建一个tag',新建一个Pointer(location,tag'),Unique(tag')压栈位于栈顶,其中location是已存在的值
规则2 use-1
任何时候一个Pointer(location,tag)被使用,tag一定是在stack里面的.如果有其他tags在它的栈位置的上面,则弹出这些tags使tag t最后位于栈顶.如果Unique(tag) 不在栈里,就是程序有未定义行为.
这个规则反映了原则:当新创建的引用在stack里面才可以使用它.这样它才可以在下一次从栈里获取使用.
记录原生指针
上述解释了如何处理可变引用,然后我们讨论如何处理原生指针(*mut T)
同样的,借用检查不会区分同一个对象的不同原始指针,因为原始指针是没有标记信息去区分的.(,一个对象可以有多个原始指针指向,不在右值上区分未标记指针)
因此我们在左值上扩展tag来标识未标记的指针
为了检测在栈上的原生指针,我们在Item上添加新的类型SharedRW
SharedRW(“共享读写”的缩写)
指示该位置已被“共享”
所有原始(未标记)指针均可访问以进行读取和写入
a special type called ⊥ ("bottom") that unifies with any type
我们将操作语义修改如下,其中UsE-2代替USE-1
规则1 new-mutable-ref1(该规则不影响 new-mutable-ref)
新:任何时候可以通过强制转换把& mut T 可变引用创建可变原始指针,即操作(* mut T),该操作有两个步骤
1.应用use-2
2.新建一个Pointer(location,⊥),SharedRW压栈位于栈顶
规则2 use-2
旧:
任何时候一个Pointer(location,tag)被使用,tag一定是在stack里面的.如果有其他tags在它的栈位置的上面,则弹出这些tags使tag t最后位于栈顶.如果Unique(tag) 不在栈里,就是程序有未定义行为.
这个规则反映了原则:当新创建的引用在stack里面才可以使用它.这样它才可以在下一次从栈里获取使用.
新:
当使用Pointer(location,tag),tag是⊥的时候,SharedRW位于location的栈中,{否则Unique(tag)一定在栈里..如果有其他tags在它的栈位置的上面,则弹出这些tags使tag t最后位于栈顶}.如果Unique(tag) 不在栈里,就是程序有未定义行为.
20行出现的错误是因为12行引用的raw_pointer不在栈顶,此时栈顶是Unique(2).因为要raw_pointer,
根据new-mutable-ref的use-1规则,弹出raw_pointer之上的Unique(2),所以后续x操作的Unique(2)没有了,违反了规则.
对于example1的栈要求如下
为了达成这个栈编码,要求Unique(3)是引用Unique(2),即只有在下一次使用Unique(2)之前使用Unique(3).即3实际上是被嵌套在2的引用(作用域)之内.这才是从2重新借用3要使用的规则.但是实际上办不到.为了确保栈上反映了从其他哪些Pointer创建了哪些Pointer,我们仅仅创建一个操作数(比如上面的1,2,3)作为对旧引用(旧操作数)的写访问.因此,栈编码改为
[. . ., SharedRW, Unique(3)]
它的编码确实是从原始指针创建的,而不是从2创建的.在之后的工作中,我们想探索其他跟踪精确指针的模型,它是基于树节点上的有指针延续信息,而不是基于栈.
重新标记和用于优化可变引用的证明草图
下一步是尝试使自己相信该模型,我们不仅为example1所需的优化排除了一个反例,而且还排除了所有可能的反例.
但是实际上终将注定失败:语义不够明确.
具体的说,如果example1被两个带有相同tag的指针值调用,那么目前为止的描述的动态分析对两者都没有问题.但是
静态分析不能区分相同的标签的两个引用.这样重复标签是可能的.因为unsafe的代码可以复制任何数据(例如使用transmute_copy),包括可变引用.
问题是我们仅考虑example1时,x和y的tag是我们提供的调用者,因此不受信任.为了能够基于tag和borrow stack进行推理.
我们需要确保两个引用都有唯一的tag.其他指针值都不会使用.(unsafe代码可以复制tag,但是不能伪造tag-没有语言操作支持)
.这是通过插入retagging指令实现的.retag是管理指令,确保引用具有新tag.它是由编译器自动插入的,特别是函数开始执行后,立即重新标记引用类型的参数.
如注释中所说,重新标记是通过重新借出所有权实现的,到目前为止,重新标记x的行为与& mut x完全相同.
这意味着x仍指向相同的位置,但是对于x使用旧值并创建新的引用(use-2,NEW-MUTABLE-REF).我们仍然遵循通常的stack借用规则.
现在我们终于可以给出证明草图,说明在此程序中第6行所需的优化是正确的.如果程序不符合stack借用规则。
则没有任何显示,因为该程序被认为具有未定义的行为。因此,让我们在程序确实符合stack借用规则的假设下进行:
(1)假设在第2行重新标记后,x的值为Pointer(location,t)。我们知道没有其他指针值具有tag t,并且tag位于t的borrow stack的顶部。
(2)x直到第4行才使用,因此如果两者之间的任何代码对值或location stack都有影响,它将通过带有不同tag的指针值来实现。该tag必须在stack中的t之下,因为我们在(1)中确定t在顶部。这意味着使用带有其他tag的指针值将弹出stack。但是,为了使第4行通过分析,t必须仍在堆栈中。从而,在第2行和第4行之间无法访问location,并且t仍必须位于t栈的顶部。在执行第4行之后,stack保持不变;但我们现在知道该值存储在location的是42。
(3)最后,在第6行中,我们可以再次重复相同的参数,以表明t是否仍在t的stack中。
在此期间,无法访问location。因此,我们可以得出结论:location仍包含值42,并且可以执行所需的优化。
关于此论点的有趣之处在于,我们从未明确讨论过x和y是否是混淆重叠的!结果,这种特定的优化可以推广到整个优化模式,其中y的访问将被任何未提及x的代码替换:
上述文字集中在描述优化,所示代码仅仅是优化流程
这种模式表明x是一个“唯一指针”。不使用x的代码可能不会影响x指向的内存。
特别地,当不使用x的代码是对未知函数的调用时,优化也适用。也就是说,堆栈借用允许我们做一些典型的C / C ++编译器无法想象的事情:我们有一个环境传入的引用x(因此指针值被“转义”了)并且仍然可以调用外部函数f(),并且只要不将x作为f的参数传递,就可以假定f既不读取也不写入x。此外,我们可以仅使用过程内推理就可以进行此假设,而无需进行任何内联处理-别名分析的“圣杯”.
retag插入代码哪里?前面示例中的retag至关重要,因为它允许编译器假定x实际上具有唯一标记。此优化以及我们将要看到的所有优化,只能在“局部”重新标记的引用上执行(在所分析的函数内),因为只有这样我们才能对标记进行必要的假设参考和它指向的位置的借用堆栈。因此,将确切的重新标记插入到何处成为stack borrow 中的一个重要点,可确定可以针对哪些引用进行优化(如上述优化)。
就目前而言,我们希望在将引用作为参数传入,从函数返回或从指针读取时,都将发生重新标记。基本上,只要引用“进入”我们的范围,就应该重新标记它: