共享引用
到目前为止,我们仅考虑了可变引用和原始指针。我们已经看到,stack borrow对可变引用强制执行一种唯一性形式,足以证明程序转换可以对未知代码周围的内存访问进行重新排序。在本节中,我们将研究共享引用。目标是强制它们是只读的,以再次证明程序转换的合理性,该转换将未知代码周围的内存访问重新排序。
就像我们为可变参考所做的一样。我们通过重述借阅检查器以避免提及生命周期的方式来
重新定义共享引用的stack原则:
每次使用引用(及其派生的所有内容)都必须在引用对象的下一次更改使用之前(之后)创建了引用),此外,不得将引用用于可变。
要了解这种情况如何发生,让我们再次考虑一个涉及整数引用的简单示例:
尽管有x,shared1和shared2别名的事实,但该程序在第8行之前都可以运行,但是在第9行,则违反了共享引用的stack原则:在第9行中使用了对shared1的引用后,才在第8行中对引用对象进行了更改.
使用Stacked Borrows进行建模,我们介绍了可以存在于Borrow stack中的另一种item
新的Item SharedRO(t)(“共享只读”)指示允许使用t标记的引用从与栈关联的位置读取但不能写入。我们还为SharedRW配备了tag。这意味着我们现在可以谈论“一个Item的tag”,这个说法在READ-1中很有用
在NEW-MUTABLE-RAW-1中,我们只把SharedRW(⊥)推入stack而不是SharedRW(目前为止,这是唯一一种SharedRW项 Item,⊥代表没有值)。
我们将现有的Stacked Borrow规则修改如下:
规则(NEW-SHARED-REF-1)。每当从某个现有指针值Pointer(location.t)创建一个新的共享引用时(&expr)。首先,这被视为对该指针值的读取访问(因此,我们遵循下面的READ-1)。然后,我们选择一些新标签t',使用Pointer(location,t')作为共享引用的值,并将SharedRO(t')添加到l的栈顶。
规则(READ-1)。每当读取值为Pointer(location,t)的指针时,tag t的Item(即Unique(t),SharedRO(t)或SharedRW(t))都必须存在于t的栈中。
将item弹出堆栈,直到tag t上方的所有item都为SharedRO(_)位置。
如果堆栈中不存在这个Item,则程序违反了stack原则。 (此规则胜过现有的use-2,后者仅用于写操作。)
请注意,我们保留写规则不变,这意味着即使SharedRO(t)在stack中,标记为t的引用也不能用于写,因为这需要Unique(t)或SharedRW(t)。
READ-1的关键点(与UsE-2的关键区别)在于,使用Pointer(location,t)进行读取不会导致标签t位于堆栈顶部的item结束!上面可能有一些SharedRO。这反映了这样一个事实,即可以使用两个共享的引用进行阅读而不会“干扰”
彼此;它们不会将另一个引用的项目弹出栈。相反,写访问(仍由use-2支配)要求带有用于访问的指针的标签的项成为堆栈中的头项。
因此,此系统保持的关键不变式是,如果栈中有任何SharedRO,则它们在顶部都相邻。请注意,所有push另一种item(创建可变引用或原始指针)的操作都算作写访问,因此它们首先会通过弹出所有SharedRO来使某些Unique()或SharedRW()成为堆栈的顶部在他们上面。从不将Unique()或SharedRW()推到SharedRO()的顶部。
使用这些规则,示例程序执行如下:
观察用于读取引用的规则如何使x,shared1和shared2愉快地共存
(甚至允许使用“ XYXY”模式),但是当我们写入x时,SharedRO()项将从堆栈中删除,并且不再使用相应的共享引用。
利用只读共享引用的优化
要查看Stacked Borrows对共享引用的处理有何帮助,让我们考虑一个可以从优化中受益的函数,利用共享引用为只读:
这次,我们使用闭包f来反映这样的想法:任意代码可以在第3行(我们第一次读取x)和第5行(我们希望优化计算)之间运行。
* x / 3。与可变引用不同,我们甚至为未知代码提供了对我们的引用x的访问权限!但是,它是一个只读引用,因此f应该不能通过它进行写操作。
再次,我们可以设计一个反例,禁止在幼稚的语义下进行此优化
在这里,我们创建一个闭包,Rust中的语法为args |。身体。有趣的部分在第6行和第7行,它们是我们传递给example2的闭包f的主体。在那里,我们通过调用transmute(Rust的sunchecked强制转换操作)来规避不能写入共享引用的限制。这使我们可以将(只读)共享引用转换为(可写)原始指针,然后进行写入。
但是,在堆积借款下,该程序具有未定义的行为(正如我们希望的那样)
转换不会影响指针的标签t或借位堆栈。从而。在第7行中,当我们遵循use-2时,在借用堆栈上找不到唯一(t)。标签为t的唯一项目是第5行中的重新标签添加的SharedRO(t)。
更详细地。这是针对example2进行优化的反例的逐步执行(闭包使控制流更加复杂-方括号中的数字表示执行的顺序):
共享参考文献优化的证明草稿
再次,我们排除了所需优化的一个特定反例,但我们真正需要做的是证明该优化在任何可能的情况下都是正确的。再次是相关代码:
现在,参数如下:
(1)假设在第2行的重新标记之后,x的值为Pointer(location,t)。我们知道SharedRO(t)
在location的借用堆栈的顶部。让我们调用存储在该位置s中的当前标量值。目的是表明在第5行中仍存储值s。
(2)在执行f的同时,我们知道任何写访问都将从堆栈中弹出所有SharedRO。
这依赖于堆栈中所有SharedRO位于顶部的不变性。因此,我们得出结论,只要SharedRO(t)在堆栈中,存储在t处的值仍为s
3)最后,在第5行中,我们假定SharedRO(t)仍在堆栈中,因为否则程序将违反Stacked Borrows(不存在带有该标签的其他项)。因此,我仍然存储s,这证明了优化的合理性。