使用赋值运算符时实际发生了什么?
What's actually happening when using assigment operator?
我有这个代码:
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
持有原始类型或任何具有 Copy 或 Clone 特征的类型,值或结构将被复制到新变量中。在这种情况下,我没有在 Point 结构中定义任何这些特征,因此我预计 b
应该拥有该结构的所有权并且不应进行任何复制。
p
和b
怎么可能有不同的内存地址?结构是否从一个地址移动到另一个地址?它是隐式复制的吗?只让 b
拥有在创建结构时已经分配的数据并因此保持相同的地址不是更有效吗?
您正在经历 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!
获取指向某物的指针需要它位于某处的可寻址内存中,这意味着编译器无法将 b
或 p
放入 CPU 寄存器(它更喜欢尽可能放东西,因为寄存器 fast)。我们甚至还没有进入优化阶段,但我们已经影响了编译器做出的关于数据需要在哪里的决定——这是一个限制优化器可以做的事情。
现在优化器必须弄清楚移动是否可以被省略。使用指向 b
和 p
的指针可以 阻止优化器这样做,但也可能不会。也有可能您只是在没有优化的情况下进行编译。
请注意,即使 Point
是 Copy
,如果您删除了所有指针打印,如果优化器可以证明 p
未被使用,它甚至可能会删除副本在副本的另一边,或者两个值都没有发生变化(这是一个很好的选择,因为它们都没有被声明 mut
)。
规则如下:永远不要试图从代码中确定编译器或优化器对您的代码做了什么——这样做实际上可能会破坏优化器并导致你得出了错误的结论。这适用于所有语言,而不仅仅是 Rust。
唯一有效的方法是查看生成的程序集。
让我们开始吧!
我以你的代码为起点,写了两个不同的函数,一个有移动,一个没有:
#![feature(bench_black_box)]
struct Point {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[inline(never)]
fn a() {
let p = Point {
x: 1.0,
y: 2.0,
z: 3.0,
};
std::hint::black_box(p);
}
#[inline(never)]
fn b() {
let p = Point {
x: 1.0,
y: 2.0,
z: 3.0,
};
let b = p;
std::hint::black_box(b);
}
fn main() {
a();
b();
}
在我们继续查看程序集之前需要指出的几件事:
std::hint::black_box()
是一个实验函数,其目的是充当优化器的黑匣子。不允许优化器查看此函数以了解它做了什么,因此它无法优化它。如果没有这个,优化器会查看函数的主体并正确地得出它根本不做任何事情的结论,并将整个事情消除为 no-op.
- 我们将这两个函数标记为
#[inline(never)]
,以确保优化器不会将这两个函数内联到main()
。这使它们更容易相互比较。
所以我们应该从中得到两个函数,我们可以比较它们的汇编。
但是我们没有得到两个函数
在生成的程序集中找不到b()
。那么发生了什么?让我们看看 main()
做了什么:
pushq %rax
callq playground::a
popq %rax
jmp playground::a
嗯...你会看那个吗?优化器发现两个函数在语义上是等价的,尽管其中一个函数有一个额外的移动。所以它决定完全消除 b()
并使它成为 a()
的别名,导致两次调用 a()
!
出于好奇,我更改了 b()
中的文字 f64
值以防止函数被统一,并看到了我期望看到的结果:除了不同的值之外,发出的程序集是完全相同的。编译器省略了移动。
(Playground -- 请注意,您需要手动按下“运行”旁边的 three-dots 按钮和 select “ASM”选项。)
我有这个代码:
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
持有原始类型或任何具有 Copy 或 Clone 特征的类型,值或结构将被复制到新变量中。在这种情况下,我没有在 Point 结构中定义任何这些特征,因此我预计 b
应该拥有该结构的所有权并且不应进行任何复制。
p
和b
怎么可能有不同的内存地址?结构是否从一个地址移动到另一个地址?它是隐式复制的吗?只让 b
拥有在创建结构时已经分配的数据并因此保持相同的地址不是更有效吗?
您正在经历 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!
获取指向某物的指针需要它位于某处的可寻址内存中,这意味着编译器无法将 b
或 p
放入 CPU 寄存器(它更喜欢尽可能放东西,因为寄存器 fast)。我们甚至还没有进入优化阶段,但我们已经影响了编译器做出的关于数据需要在哪里的决定——这是一个限制优化器可以做的事情。
现在优化器必须弄清楚移动是否可以被省略。使用指向 b
和 p
的指针可以 阻止优化器这样做,但也可能不会。也有可能您只是在没有优化的情况下进行编译。
请注意,即使 Point
是 Copy
,如果您删除了所有指针打印,如果优化器可以证明 p
未被使用,它甚至可能会删除副本在副本的另一边,或者两个值都没有发生变化(这是一个很好的选择,因为它们都没有被声明 mut
)。
规则如下:永远不要试图从代码中确定编译器或优化器对您的代码做了什么——这样做实际上可能会破坏优化器并导致你得出了错误的结论。这适用于所有语言,而不仅仅是 Rust。
唯一有效的方法是查看生成的程序集。
让我们开始吧!
我以你的代码为起点,写了两个不同的函数,一个有移动,一个没有:
#![feature(bench_black_box)]
struct Point {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[inline(never)]
fn a() {
let p = Point {
x: 1.0,
y: 2.0,
z: 3.0,
};
std::hint::black_box(p);
}
#[inline(never)]
fn b() {
let p = Point {
x: 1.0,
y: 2.0,
z: 3.0,
};
let b = p;
std::hint::black_box(b);
}
fn main() {
a();
b();
}
在我们继续查看程序集之前需要指出的几件事:
std::hint::black_box()
是一个实验函数,其目的是充当优化器的黑匣子。不允许优化器查看此函数以了解它做了什么,因此它无法优化它。如果没有这个,优化器会查看函数的主体并正确地得出它根本不做任何事情的结论,并将整个事情消除为 no-op.- 我们将这两个函数标记为
#[inline(never)]
,以确保优化器不会将这两个函数内联到main()
。这使它们更容易相互比较。
所以我们应该从中得到两个函数,我们可以比较它们的汇编。
但是我们没有得到两个函数
在生成的程序集中找不到b()
。那么发生了什么?让我们看看 main()
做了什么:
pushq %rax
callq playground::a
popq %rax
jmp playground::a
嗯...你会看那个吗?优化器发现两个函数在语义上是等价的,尽管其中一个函数有一个额外的移动。所以它决定完全消除 b()
并使它成为 a()
的别名,导致两次调用 a()
!
出于好奇,我更改了 b()
中的文字 f64
值以防止函数被统一,并看到了我期望看到的结果:除了不同的值之外,发出的程序集是完全相同的。编译器省略了移动。
(Playground -- 请注意,您需要手动按下“运行”旁边的 three-dots 按钮和 select “ASM”选项。)