所有权和有条件执行的代码
Ownership and conditionally executed code
我周末读了 rust 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!
s 并为装箱值实现 Drop
特性,我看到包含值 1 的框将根据 return 的值在第 6 行或第 7 行释放flip_coin
。编译器如何知道何时释放该框?这是在 运行 时使用一些 运行 时的信息决定的(比如一个标志来指示盒子是否仍在使用)?
在非优化代码中,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");
}
与您对其他答案的评论一致,在 "leaving inner scope" println 之后对单独留下的字符串调用 drop
。这似乎与 y 的范围延伸到其块末尾的期望一致。
查看未经优化编译的汇编语言,似乎 if
语句不仅将 y1 或 y2 复制到 x,而且还将提供移动源的任何变量置零。这是测试:
.LBB14_8:
movb -437(%rbp), %al
andb , %al
movb %al, -177(%rbp)
testb , -177(%rbp)
jne .LBB14_11
jmp .LBB14_12
这是 'then' 分支,它将 "y1" 字符串移动到 x。特别注意对 memset
的调用,它在移动后将 y1 清零:
.LBB14_11:
xorl %esi, %esi
movl , %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 的 24 个字节(一个指针加上两个使用)。这是因为 Rust 在结构中添加了一个隐藏的 "drop flag",指示该值是否存在,紧跟三个可见字段
这里是 'else' 分支,对 y2 执行完全相同的操作:
.LBB14_12:
xorl %esi, %esi
movl , %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:
后面是 "leaving inner scope" println 的代码,看起来很痛苦,所以我不会在这里包含它。
然后我们在 y1 和 y2 上调用 "glue_drop" 例程。这似乎是一个编译器生成的函数,它接受一个 A,检查它的字符串的 Vec 的丢弃标志,如果设置了,则调用 A 的丢弃例程,然后是它包含的字符串的丢弃例程。
如果我没看错的话,它非常聪明:即使 A 具有我们需要先调用的 drop
方法,Rust 知道它可以使用 ... inhale ... A里面String里面的Vec的drop flag作为A是否需要被drop的标志。
现在,当使用优化编译时,内联和流分析应该识别丢弃肯定会发生(并省略 运行-时间检查)或绝对不会发生(并完全忽略丢弃)的情况).而且我相信我听说过将 then/else 子句后面的代码复制到两个路径中的优化,然后专门化它们。这将从该代码中消除所有 运行 时间检查(但重复 println! 调用)。
正如最初的发帖人所指出的,有一项 RFC 提议将丢弃标志从值中移出,而是将它们与保存值的堆栈槽相关联。
因此优化代码可能根本没有任何 运行 时间检查是合理的。不过,我无法让自己阅读优化后的代码。为什么不亲自尝试一下呢?
经过一些研究,我发现 Rust currently adds a flag to every type that implements the Drop
trait so that it knows whether the value has been dropped or not, which of course incurs a run-time cost. There have been proposals to avoid that cost by using static drops or eager drops but those solutions had problems with their semantics, namely that drops could occur at places that you wouldn't expect (e.g. in the middle of a code block), especially if you are used to C++ style RAII. There is now consensus that the best compromise is a different solution 从类型中删除了标志。相反,标志将被添加到堆栈中,但只有当编译器无法确定何时静态执行 drop
时(同时具有与 C++ 相同的语义),这特别发生在有条件移动时,如本示例中给出的题。对于所有其他情况,不会有 运行 时间成本。不过,该提案似乎不会在 1.0 中及时实施。
请注意,C++ 具有与 unique_ptr
相似的 运行 时间成本。当新 Drop
实现时,Rust 在这方面将严格优于 C++。
我希望这是对情况的正确总结。感谢 u/dyoll1013、u/pcwalton、u/!!kibwen、reddit 上的 u/Kimundi,以及 SO 上的 Chris Morgan。
我周末读了 rust 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!
s 并为装箱值实现 Drop
特性,我看到包含值 1 的框将根据 return 的值在第 6 行或第 7 行释放flip_coin
。编译器如何知道何时释放该框?这是在 运行 时使用一些 运行 时的信息决定的(比如一个标志来指示盒子是否仍在使用)?
在非优化代码中,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");
}
与您对其他答案的评论一致,在 "leaving inner scope" println 之后对单独留下的字符串调用 drop
。这似乎与 y 的范围延伸到其块末尾的期望一致。
查看未经优化编译的汇编语言,似乎 if
语句不仅将 y1 或 y2 复制到 x,而且还将提供移动源的任何变量置零。这是测试:
.LBB14_8:
movb -437(%rbp), %al
andb , %al
movb %al, -177(%rbp)
testb , -177(%rbp)
jne .LBB14_11
jmp .LBB14_12
这是 'then' 分支,它将 "y1" 字符串移动到 x。特别注意对 memset
的调用,它在移动后将 y1 清零:
.LBB14_11:
xorl %esi, %esi
movl , %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 的 24 个字节(一个指针加上两个使用)。这是因为 Rust 在结构中添加了一个隐藏的 "drop flag",指示该值是否存在,紧跟三个可见字段
这里是 'else' 分支,对 y2 执行完全相同的操作:
.LBB14_12:
xorl %esi, %esi
movl , %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:
后面是 "leaving inner scope" println 的代码,看起来很痛苦,所以我不会在这里包含它。
然后我们在 y1 和 y2 上调用 "glue_drop" 例程。这似乎是一个编译器生成的函数,它接受一个 A,检查它的字符串的 Vec 的丢弃标志,如果设置了,则调用 A 的丢弃例程,然后是它包含的字符串的丢弃例程。
如果我没看错的话,它非常聪明:即使 A 具有我们需要先调用的 drop
方法,Rust 知道它可以使用 ... inhale ... A里面String里面的Vec的drop flag作为A是否需要被drop的标志。
现在,当使用优化编译时,内联和流分析应该识别丢弃肯定会发生(并省略 运行-时间检查)或绝对不会发生(并完全忽略丢弃)的情况).而且我相信我听说过将 then/else 子句后面的代码复制到两个路径中的优化,然后专门化它们。这将从该代码中消除所有 运行 时间检查(但重复 println! 调用)。
正如最初的发帖人所指出的,有一项 RFC 提议将丢弃标志从值中移出,而是将它们与保存值的堆栈槽相关联。
因此优化代码可能根本没有任何 运行 时间检查是合理的。不过,我无法让自己阅读优化后的代码。为什么不亲自尝试一下呢?
经过一些研究,我发现 Rust currently adds a flag to every type that implements the Drop
trait so that it knows whether the value has been dropped or not, which of course incurs a run-time cost. There have been proposals to avoid that cost by using static drops or eager drops but those solutions had problems with their semantics, namely that drops could occur at places that you wouldn't expect (e.g. in the middle of a code block), especially if you are used to C++ style RAII. There is now consensus that the best compromise is a different solution 从类型中删除了标志。相反,标志将被添加到堆栈中,但只有当编译器无法确定何时静态执行 drop
时(同时具有与 C++ 相同的语义),这特别发生在有条件移动时,如本示例中给出的题。对于所有其他情况,不会有 运行 时间成本。不过,该提案似乎不会在 1.0 中及时实施。
请注意,C++ 具有与 unique_ptr
相似的 运行 时间成本。当新 Drop
实现时,Rust 在这方面将严格优于 C++。
我希望这是对情况的正确总结。感谢 u/dyoll1013、u/pcwalton、u/!!kibwen、reddit 上的 u/Kimundi,以及 SO 上的 Chris Morgan。