我在周末阅读了铁锈book,我对所有权概念有疑问。我得到的印象是所有权用于静态确定资源可以解除分配的位置。现在,假设我们有以下内容:
{ // 1
let x; // 2
{ // 3
let y = Box::new(1); // 4
x = if flip_coin() {y} else {Box::new(2)} // 5
} // 6
} // 7
我很惊讶地发现编译器接受了这个程序。通过插入println!
并为盒装值实现Drop
特征,我看到包含值1的框将在第6行或第7行解除分配,具体取决于{{1}的返回值}。编译器如何知道何时解除分配该框?这是在运行时使用一些运行时信息决定的(比如标志是否仍然在使用该框)?
答案 0 :(得分:6)
经过一些研究后,我发现Rust currently为每个实现Drop
特征的类型添加了一个标志,以便它知道该值是否已被删除,这当然会导致运行 - 时间成本。有人建议通过使用static drops或eager drops来避免这种成本,但这些解决方案的语义存在问题,即可能会出现在您不期望的地方(例如在中间)代码块),特别是如果你习惯了C ++风格的RAII。现在已经达成共识,最好的折衷方案是different solution,其中标志从类型中删除。相反,标志将被添加到堆栈中,但仅当编译器无法确定何时静态执行drop
(同时具有与C ++相同的语义)时,如果存在条件移动(如此处给出的示例)题。对于所有其他情况,将没有运行时成本。但是,似乎该提案不会及时实施1.0。
请注意,C ++具有与unique_ptr
相关的类似运行时成本。实现新的Drop
时,Rust在这方面将严格优于C ++。
我希望这是对情况的正确总结。归功于u / dyoll1013,u / pcwalton,u / !! kibwen,u / Kimundi on reddit,Chris Morgan来自SO。
答案 1 :(得分:4)
在非优化代码中,Rust使用动态检查,但很可能会在优化代码中消除它们。
我查看了以下代码的行为:
#[derive(Debug)]
struct A {
s: String
}
impl Drop for A {
fn drop(&mut self) {
println!("Dropping {:?}", &self);
}
}
fn flip_coin() -> bool { false }
#[allow(unused_variables)]
pub fn test() {
let x;
{
let y1 = A { s: "y1".to_string() };
let y2 = A { s: "y2".to_string() };
x = if flip_coin() { y1 } else { y2 };
println!("leaving inner scope");
}
println!("leaving middle scope");
}
与您对另一个答案的评论一致,在{离开内部范围“println之后发生了对drop
的单独调用的字符串的调用。这似乎与人们期望y的范围延伸到他们的区块结束时一致。
查看汇编语言,在没有优化的情况下编译,似乎if
语句不仅将y1或y2复制到x,而且还将任何变量提供给移动源。这是测试:
.LBB14_8:
movb -437(%rbp), %al
andb $1, %al
movb %al, -177(%rbp)
testb $1, -177(%rbp)
jne .LBB14_11
jmp .LBB14_12
这是'then'分支,它将“y1”字符串移动到x。请特别注意对memset
的调用,它在移动后将y1清零:
.LBB14_11:
xorl %esi, %esi
movl $32, %eax
movl %eax, %edx
leaq -64(%rbp), %rcx
movq -64(%rbp), %rdi
movq %rdi, -176(%rbp)
movq -56(%rbp), %rdi
movq %rdi, -168(%rbp)
movq -48(%rbp), %rdi
movq %rdi, -160(%rbp)
movq -40(%rbp), %rdi
movq %rdi, -152(%rbp)
movq %rcx, %rdi
callq memset@PLT
jmp .LBB14_13
(直到你意识到所有这些movq
指令只是从%rbp-64
复制32个字节(即y1)到%rbp-176
,这是x,或者至少是一些临时的,最终会是x。)注意它复制32个字节,而不是你期望的Vec(一个指针加上两个usize)。这是因为Rust在结构中添加了一个隐藏的“drop flag”,指示值是否有效,跟随三个可见字段。
这里是'else'分支,为y2完全相同:
.LBB14_12:
xorl %esi, %esi
movl $32, %eax
movl %eax, %edx
leaq -128(%rbp), %rcx
movq -128(%rbp), %rdi
movq %rdi, -176(%rbp)
movq -120(%rbp), %rdi
movq %rdi, -168(%rbp)
movq -112(%rbp), %rdi
movq %rdi, -160(%rbp)
movq -104(%rbp), %rdi
movq %rdi, -152(%rbp)
movq %rcx, %rdi
callq memset@PLT
.LBB14_13:
接下来是“离开内部范围”println的代码,这很难看,所以我不会在这里包含它。
然后我们在y1和y2上调用“glue_drop”例程。这似乎是一个编译器生成的函数,它接受一个A,检查它的String的Vec的drop标志,如果设置了,则调用A的drop例程,然后调用它包含的String的drop例程。
如果我正确读这个,那就非常聪明:即使它是A,我们需要先调用drop
方法,Rust知道它可以使用...... 吸气 ... A中String内Vec的drop标志,作为指示是否需要删除A的标志。
现在,在使用优化进行编译时,内联和流分析应该识别肯定会发生丢弃的情况(并省略运行时检查),或者肯定不会发生(并且完全省略掉线)。我相信我听说过将then / else子句后面的代码复制到两个路径中的优化,然后将它们专门化。这将消除此代码中的所有运行时检查(但重复println!调用)。
正如原始海报所指出的那样,有RFC提议将drop标志移出值,而是将它们与保存值的堆栈槽相关联。
因此,优化的代码可能根本没有任何运行时检查。但是,我不能让自己阅读优化代码。为什么不亲自尝试一下?