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

7
我在周末阅读了Rust的《Rust编程之道》,对所有权这一概念有些疑惑。我的理解是,所有权用于静态地确定资源的释放位置。现在,假设我们有以下代码:
{                                                 // 1
    let x;                                        // 2
    {                                             // 3
        let y = Box::new(1);                      // 4
        x = if flip_coin() {y} else {Box::new(2)} // 5
    }                                             // 6
}                                                 // 7

我很惊讶地发现编译器接受了这个程序。通过插入println!并为包含值1的盒子实现Drop特质,我看到该盒子将在第6或第7行根据flip_coin的返回值被回收。编译器如何知道何时回收该盒子?这是使用某些运行时信息(例如指示盒子是否仍在使用的标志)在运行时决定的吗?


1
目前,使用了放弃标志,但我不认为这是唯一正确的行为;有人提出了在几个方向中改变它的想法。 - Chris Morgan
2个回答

7

经过一些研究,我发现 Rust 目前 对实现Drop特质的每种类型都添加了一个标志,以便知道该值是否已被删除,这当然会产生运行时成本。曾经提出使用静态删除急切删除来避免这种成本,但这些解决方案在其语义方面存在问题,即删除可能发生在您不希望的地方(例如在代码块中间),特别是如果您习惯于 C++ 风格的 RAII。现在达成共识的最佳折衷方案是一种不同的解决方案,其中从类型中删除了标志。相反,只有在编译器无法静态地确定何时执行drop(同时具有与 C++ 相同的语义)时,才会将标志添加到堆栈中,这特别发生在存在条件移动的情况下,例如在此问题中给出的示例。对于所有其他情况,都不会产生运行时成本。然而,似乎这个提案不会及时实施到 1.0 版本中。
请注意,C ++与“unique_ptr”相关的运行时成本类似。当新的“Drop”实现时,Rust在这方面将严格优于C ++。
我希望这是对情况的正确总结。感谢Reddit上的u / dyoll1013,u / pcwalton,u / !!kibwen,u / Kimundi以及SO上的Chris Morgan。

1
栈上放置标志现在已在Rust中实现。 - Kornel

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");
}

与您在其他答案中的评论一致,对于未改变的字符串调用drop发生在“离开内部作用域”的println之后。这似乎符合人们的期望,即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(即y1)复制32个字节到%rbp-176(即x),或者至少是一些最终将成为x的临时变量之前,它看起来很可怕。)请注意,它复制了32个字节,而不是您期望的Vec的24个字节(一个指针加上两个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例程。

如果我理解得正确,这是相当聪明的:尽管需要首先调用具有drop方法的A,但Rust知道它可以使用... 吸气... A内部String中的Vec的drop标志作为指示A是否需要被删除的标志。

现在,当进行优化、内联和流分析编译时,应该能够识别出确实会发生drop的情况(并省略运行时检查),或者绝对不会发生drop的情况(并完全省略drop)。我相信我听说过优化可以将then/else子句后面的代码复制到两个路径中,然后对它们进行专门处理。这将从此代码中消除所有运行时检查(但会复制println!调用)。

正如原帖作者所指出的那样,有一个RFC提议将drop标志移出值,并与保存值的栈槽相关联。
因此,优化后的代码可能根本没有任何运行时检查。不过我自己无法阅读优化后的代码。为什么不自己尝试一下呢?

Rust代码充满了移动操作,这在源代码层面上很明显,我的答案也展示了它们在机器语言层面上的出现。然而,我期望基于SSA的代码生成器(如LLVM)在被要求进行优化时通常会非常擅长消除它们:SSA应该能够直接看到值的所有中间位置。我想知道是否真的如此... - Jim Blandy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接