存活性分析(Liveness Analysis)
写于2022年1月23日。最后更新于2022年2月7日。
存活性分析主要计算出在MIR的各个位置处有哪些借用源(Origin)存活,用于下一阶段的借债分析。
输入
1. 普通变量的定义、使用、丢弃情况。
.decl var_defined_at(Variable, Point)
.decl var_used_at (Variable, Point)
.decl var_dropped_at(Variable, Point)
.input var_defined_at
.input var_used_at
.input var_dropped_at
2. 对于引用变量而言,需要输入变量所关联的借用源。
如下
let a: i32 = 0;
let b: &'b i32 = &a;
其中b
变量将关联到借用源'b
。
另外还有丢弃时关联借用源(详见思考3)。
.decl drop_of_var_derefs_origin(Variable, Origin)
.decl use_of_var_derefs_origin (Variable, Origin)
.input drop_of_var_derefs_origin
.input use_of_var_derefs_origin
推导
1. 变量存活情况
对于每一个变量,从每个 使用 的位置处,沿CFG 逆边 向前追溯到变量 定义 处,都视为存活。
var_live_on_entry(Variable, Point) :-
var_used_at(Variable, Point).
var_live_on_entry(Variable, SourcePoint) :-
var_live_on_entry(Variable, Targetpoint),
cfg_edge(SourcePoint, TargetPoint),
!var_defined(Variable, SourcePoint).
2. 将变量的半初始化状态沿CFG传播。
var_maybe_partly_initialized_on_entry(Variable, TargetPoint) :-
var_maybe_partly_initialized_on_exit(Variable, SourcePoint),
cfg_edge(SourcePoint, TargetPoint).
3. 变量的丢弃前存活情况
对于每一个变量,在其 半初始化 的范围内,从每个 丢弃 处开始,沿CFG 逆边 向前追溯到变量 定义 处,都视为 丢弃前存活(drop-live)。
var_drop_live_on_entry(Variable, Point) :-
var_dropped_at(Variable, Point),
var_maybe_partly_initialized_on_entry(Variable, Point).
var_drop_live_on_entry(Variable, SourcePoint) :-
var_drop_live_on_entry(Variable, TargetPoint),
cfg_edge(SourcePoint, TargetPoint),
!var_defined_at(Variable SourcePoint),
var_maybe_partly_initialized_on_exit(Variable, SourcePoint).
4. 借用源存活状态
对每处的存活引用变量,都标记其关联的借用源为存活。
origin_live_on_entry(Origin, Point) :-
var_live_on_entry(Variable, Point),
use_of_var_derefs_origin(Variable, Origin).
同样地,对于丢弃前存活的引用变量,也标记其相关联的丢弃时借用源为存活。
origin_live_on_entry(Origin, Point) :-
var_drop_live_on_entry(Variable, Point),
drop_of_var_derefs_origin(Variable, Origin).
思考(部份内容来自Zulip的讨论)
1. 既然已经有基于路径的一些输入事实(如path_assigned_at
、path_accessed_at
、path_moved_at
等),并且也有path_is_var
能与变量对应起来,为什么还需要基于变量的输入事实呢(如var_used_at
、var_dropped_at
、var_defined_at
等)?
第一阶段的初始化分析,与具体的字段相关;而第二阶段的存活性分析与第三阶段的借债分析都只与变量相关。对存活性分析而言,变量相关的输入事实是必须的、而字段相关的输入信息是可选的,后续可能移除字段相关的输入,但变量相关的输入不会变。
具体的区别有:
(1) path_assigned_at
与var_defined_at
path_assigned_at
与var_defined_at
的最大区别是:- 在MIR的每一条赋值语句中,
var_defined_at
一定发生在当前语句中间(Mid
); - 而
path_assigned_at
可能发生在当前赋值语句中间(Mid
),或下一条语句之前(Start
),- 若当前语句不需要
unwind
,则path_assigned_at
在当前赋值语句中间; - 若当前语句需要
unwind
,则path_assigned_at
在执行成功的分支中下一条语句之前。
- 若当前语句不需要
- 在MIR的每一条赋值语句中,
var_defined_at
还包含StorageLive
与StorageDead
信息,及变量被赋值的信息。path_assigned_at
还包含进入函数时,对入参的初始化赋值。
在初始化分析中,当某些函数调用发生了panic
时,其返回值不会被初始化,因此path_assigned_at
发生在执行成功后下一条语句之前。
但存活性分析及后续的借债分析,并不在意在发生panic
后变量是否处于未初始化状态。
- 对于
var_live_on_entry
而言,在某条赋值语句(如_3 = may_panic() [return -> bb1; unwind -> bb2]
)发生panic
后,其「清理」路径(从bb2
开始,依次丢弃已初始化的变量)并不会使用到该变量(_3
),也不会产生相应的var_defined_at
事实,因此即使发生了panic
,产生的var_defined_at
在unwind
过程中并不会产生var_live_on_entry
事实,所以此处的var_defined_at
不需要像path_assigned_at
那样,必须发生在非unwind
分支的入口处。 - 对于
var_drop_live_on_entry
而言,具体由于unwind
过程中可能产生var_dropped_at
事实,因此有可能影响到var_drop_live_on_entry
,所以具体情况有待进一步实验验证。
(2) path_accessed_at
与var_used_at
var_userd_at
等于path_accessed_at
加上返回值的访问信息。
(3) path_moved_at
与var_dropped_at
path_moved_at
只能追踪到其赋值与移动。而变量在超出其作用域后,还会被丢弃,无法通过路径的移动信息推导出变量的丢弃信息。
2. 为什么var_defined_at
中包含StorageLive
和StorageDead
?
StorageLive
与StorageDead
主要用于LLVM栈空间分配。见MIR相关文档。
StorageLive(_1)
表明变量_1
存活,也即其可能在稍后使用——直到StorageDead(_1)
语句出现,即_1
将不再使用。StorageLive
与StoraageDead
语句用于LLVM中的栈空间分配。
在StorageLive
之后,变量才开始使用,此处的var_defined_at
可以阻止存活性分析中的var_live_on_entry
追溯到早于StorageLive
的地方(因为这些地方变量未被使用到)。
StorageDead
不太必要,因为在StorageDead
后,将不再可能出现变量被访问的情形。
3. 为什么要区分drop_of_var_derefs_origin
与use_of_var_derefs_origin
?
由于rfc#1327的缘故,变量在丢弃时的使用情况状态与正常使用时的使用情况有所不同。
drop_of_var_derefs_origin
的含义是,变量在丢弃时,由于其drop
函数中可能访问其包含的引用,因此需要将其视作存活,除非显式指明#[may_dangle]
。
例如:
fn main() {
let mut v = [1, 2, 3];
let p: Wrapped<& /* R4 */ usize> = Wrapped { value: &v[0] };
if true {
drop(*p.value);
} else {
v[0] += 1; //~ ERROR cannot assign to `v[_]` because it is borrowed
}
v[0] += 1; //~ ERROR cannot assign to `v[_]` because it is borrowed
}
struct Wrapped<T> {
value: T
}
impl<T> Drop for Wrapped<T> {
fn drop(&mut self) { }
}
由于在Wrapped::drop
中可能访问到value
字段(无显式访问,但在丢弃其字段时会隐式访问到),因此需要drop_of_var_derefs_origin
输入事实,表明p.value
借用的变量&v[0]
直到p
被丢弃时(main
函数结束前),一直处于存活状态。
若将impl<T> Drop for Wrapped<T>
改为
unsafe impl<#[may_dangle] T> Drop for Wrapped<T> {
fn drop(&mut self) { }
}
则不产生drop_of_var_derefs_origin
事实,因而后面v[0] += 1
也视为合法。
4. 为什么var_drop_live_on_entry
要包含var_maybe_partly_initialized_on_entry
的信息?
即使变量出作用域时已被移动,而原始的MIR中仍然包含相应的drop
语句。Polonius引擎(包括此前的借用检查引擎)手动过滤了所有的「移动后丢弃」语句。但变量变量在部份移动后,开始处于「丢弃前存活」的状态,且部份丢弃的变量不会产生var_dropped_at
事实,因此需要var_maybe_partly_initialized_on_exit
来补上变量的部份移动信息。