所有权和有条件执行的代码

时间:2015-03-09 17:58:11

标签: rust ownership

我在周末阅读了铁锈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}的返回值}。编译器如何知道何时解除分配该框?这是在运行时使用一些运行时信息决定的(比如标志是否仍然在使用该框)?

2 个答案:

答案 0 :(得分:6)

经过一些研究后,我发现Rust currently为每个实现Drop特征的类型添加了一个标志,以便它知道该值是否已被删除,这当然会导致运行 - 时间成本。有人建议通过使用static dropseager 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标志移出值,而是将它们与保存值的堆栈槽相关联。

因此,优化的代码可能根本没有任何运行时检查。但是,我不能让自己阅读优化代码。为什么不亲自尝试一下?