使用赋值运算符时实际发生了什么?
我有这样的代码:
struct Point {
pub x: f64,
pub y: f64,
pub z: f64,
}
fn main() {
let p = Point {
x: 1.0,
y: 2.0,
z: 3.0,
};
println!("{:p}", &p);
println!("{:p}", &p.x); // To make sure I'm seeing the struct address and not the variable address. </paranoid>
let b = p;
println!("{:p}", &b);
}
可能的输出:
0x7ffe631ffc28
0x7ffe631ffc28
0x7ffe631ffc90
我试图了解执行 let b = p
时会发生什么。我知道,如果 p
拥有原始类型或任何具有复制或克隆特征的类型,则值或结构将被复制到新变量中。在本例中,我没有在 Point 结构中定义任何这些特征,因此我希望 b
应该取得该结构的所有权,并且不应进行任何复制。
p
和 b
怎么可能有不同的内存地址?该结构是否从一个地址移动到另一个地址?它是隐式复制的吗?让 b
拥有创建结构时已分配的数据并因此保持相同的地址难道不应该更有效吗?
I have this code:
struct Point {
pub x: f64,
pub y: f64,
pub z: f64,
}
fn main() {
let p = Point {
x: 1.0,
y: 2.0,
z: 3.0,
};
println!("{:p}", &p);
println!("{:p}", &p.x); // To make sure I'm seeing the struct address and not the variable address. </paranoid>
let b = p;
println!("{:p}", &b);
}
Possible output:
0x7ffe631ffc28
0x7ffe631ffc28
0x7ffe631ffc90
I'm trying to understand what happens when doing let b = p
. I know that, if p
holds a primitive type or any type with the Copy or Clone traits, the value or struct is copied into the new variable. In this case, I have not defined any of those traits in the Point structure, so I expected that b
should take the ownership of the struct and no copy should be made.
How is it possible p
and b
to have different memory addresses? Are the struct moved from one address to another? Is it implicitly copied? Shouldn't be more efficient to just make b
own the data that has already been allocated when creating the structure, and therefore maintaining the same address?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您正在体验观察者效应:通过获取指向这些字段的指针(发生这种情况)当您使用
{:p}
格式化引用时,您会导致编译器和优化器改变其行为。 您通过测量改变了结果!获取指向某个对象的指针要求它位于可寻址内存中某处,这意味着编译器无法将
b
或p
放入 CPU 寄存器中(它更愿意在可能的情况下将内容放入其中,因为寄存器速度很快)。我们甚至还没有进入优化阶段,但我们已经影响了编译器做出的关于数据需要放在哪里的决策——这是限制优化器可以做的一件大事。现在优化器必须弄清楚是否可以省略该移动。使用指向
b
和p
的指针可能阻止优化器这样做,但也可能不会。也有可能您只是在没有优化的情况下进行编译。请注意,即使
Point
是Copy
,如果您删除了所有指针打印,优化器甚至可能会删除副本(如果它可以证明p
) > 要么在副本的另一侧未使用,要么两个值都没有发生变化(这是一个很好的选择,因为两个值都没有声明mut
)。规则如下:永远不要尝试从代码中确定编译器或优化器对您的代码执行的操作 - 这样做实际上可能会破坏优化器并导致您得出错误的结论。这适用于所有语言,而不仅仅是 Rust。
唯一有效的方法是查看生成的程序集。
那么让我们这样做吧!
我使用您的代码作为起点,编写了两个不同的函数,一个带有移动,一个没有:
在我们继续查看程序集之前需要指出的一些事情:
std::hint::black_box()
是一个实验性函数,其目的是充当优化器的黑匣子。优化器不允许查看这个函数来了解它的作用,因此它无法优化它。如果没有这个,优化器将查看函数的主体并正确地得出结论:它根本没有做任何事情,并将整个事情作为无操作消除。main()
中。这使得它们更容易相互比较。所以我们应该从中得到两个函数,然后我们可以比较它们的汇编。
但是我们没有得到两个函数。
在生成的程序集中,找不到
b()
。那么到底发生了什么?让我们看看main()
做了什么:嗯...你能看一下吗?优化器发现两个函数在语义上是等效的,尽管其中一个函数有额外的移动。因此它决定完全消除
b()
并使其成为a()
的别名,从而导致两次调用a()
!出于好奇,我更改了
b()
中的文字f64
值,以防止函数统一,并看到了我期望看到的内容:除了不同的值之外,发出的组件是相同的。编译器忽略了这一举动。(游乐场 -- 注意您需要手动按“运行”旁边的三点按钮并选择“ASM” 选项。)
You are experiencing the observer effect: by taking a pointer to these fields (which happens when you format a reference with
{:p}
) you have caused both the compiler and the optimizer to alter their behavior. You changed the outcome by measuring it!Taking a pointer to something requires that it be in addressable memory somewhere, which means the compiler couldn't put
b
orp
in CPU registers (where it prefers to put stuff when possible, because registers are fast). We haven't even gotten to the optimization stage but we've already affected decisions the compiler has made about where the data needs to be -- that's a big deal that limits what the optimizer can do.Now the optimizer has to figure out whether the move can be elided. Taking pointers to
b
andp
could prevent the optimizer from doing so, but it may not. It's also possible that you're just compiling without optimizations.Note that even if
Point
wereCopy
, if you removed all of the pointer printing, the optimizer may even elide the copy if it can prove thatp
is either unused on the other side of the copy, or neither value is mutated (which is a pretty good bet since neither are declaredmut
).Here's the rule: don't ever try to determine what the compiler or optimizer does with your code from within that code -- doing so may actually subvert the optimizer and lead you to a wrong conclusion. This applies to every language, not just Rust.
The only valid approach is to look at the generated assembly.
So let's do that!
I used your code as a starting point and wrote two different functions, one with the move and one without:
A few things to point out before we move on to look at the assembly:
std::hint::black_box()
is an experimental function whose purpose is to act as a, well, black box to the optimizer. The optimizer is not allowed to look into this function to see what it does, therefore it cannot optimize it away. Without this, the optimizer would look at the body of the function and correctly conclude that it doesn't do anything at all, and eliminate the whole thing as a no-op.#[inline(never)]
to ensure that the optimizer won't inline both functions intomain()
. This makes them easier to compare to each other.So we should get two functions out of this and we can compare their assembly.
But we don't get two functions.
In the generated assembly,
b()
is nowhere to be found. So what happened instead? Let's look to see whatmain()
does:Well... would you look at that. The optimizer figured out that two functions are semantically equivalent, despite one of them having an additional move. So it decided to completely eliminate
b()
and make it an alias fora()
, resulting in two calls toa()
!Out of curiosity, I changed the literal
f64
values inb()
to prevent the functions from being unified and saw what I expected to see: other than the different values, the emitted assembly was identical. The compiler elided the move.(Playground -- note that you need to manually press the three-dots button next to "run" and select the "ASM" option.)