fn main() {
let mut v1 = vec![1, 2, 3, 4];
v1[2] = 5;
let mut v2 = v1;
v2[3] = 6;
println!("{:?}", v2);
}
v1
is moved to v2
.
Will v1
be replaced with v2
at compile time, or they are two different references on stack?
fn main() {
let mut v1 = vec![1, 2, 3, 4];
v1[2] = 5;
let mut v2 = v1;
v2[3] = 6;
println!("{:?}", v2);
}
v1
is moved to v2
.
Will v1
be replaced with v2
at compile time, or they are two different references on stack?
The vec v1
itself consists of a triple pointer+len+capacity on the stack and data behind that pointer on the heap. A move of v1
to v2
will usually copy the stuff that's on the stack into the new variable, i.e. v2
is a different place on the stack that's also big enough to hold the (pointer, length, capacity) triple and the move will copy this triple from one place to the other.
The "move" part of moving then is achieved by the fact that the compiler knows and remembers that v1 cannot be used anymore after the move (unless it's a mutable variable, then you can move a new vector into it which will overwrite the invalidated triple that points to the same vec that's logically owned by v2
). You can think of the data in v1
after the move like garbage data, as if the variable is uninitialized again.
After v1
is moved out of, there will also not be any destructor calls on its old value when it goes out of scope or when it's assigned a new value. In tricky situations where it isn't statically known anymore whether at a particular place in the program v1 has been moved out of, the compiler will insert some extra "drop flag" boolean value on the stack that indicates whether v1
still contains a value that may be accessed and has to be dropped or whether all it contains is "garbage" that must not be accessed through v1
anymore. In such a situation the move itself will also result in this flag being updated.
Finally, and crucially, all this can change after compiler optimizations. The compiler may very well decide to make v1
and v2
actually use the same place on the stack, or even just the same registers without any stack usage, as long as such a change does not affect the program behavior. And in this case the move becomes a no-OP. Avoiding moves like that is in particular common for arguments and return values of a function, in case the function call gets inlined.
So in your code example, since v1
and v2
are never containing a (not moved out) value at the same time, I would be fairly confident that the compiler is turning the two variables into a single one and removing that move operation so that it does nothing at runtime. (In a release build only, where optimizations are turned on.)
This is new to me. Do you have any reference for this?
I always thought this would be what some of the wrapper types are for.
Would this be something that triggers this behavior (without optimizations of course):
fn too<T: Default>(a:T) -> T {
let b = if complicated_runtime_bool(){
a
} else {
Default::default()
}
b
}
I don't know for sure, but it would probably just be turned into this.
fn too<T: Default>(mut a: T) -> T {
if !complicated_runtime_bool() {
drop(a);
a = Default::default();
}
a
}
Regarding the no-optimizations thing, you need some optimizations to compile code at all, so you cannot truly have no optimizations. Assembly code does not have if-else. They have conditional goto, and that's it.
I already thought something g like that would happen, but I thought it served as an example
Perhaps unnecessarily pedantic but that really depends on the architecture. ARM can have conditional execution, x86 has conditional moves.
And even though not practical, you can compile any program into a sequence of nothing else but unconditional mov instructions (MoVfuscator).
Let’s try looking at the intermediate representation. Without knowing anything about the MIR syntax myself, let’s run
pub fn complicated_runtime_bool() -> bool {
true
}
pub fn too<T: Default>(a: T) -> T {
let b = if complicated_runtime_bool() {
a
} else {
Default::default()
};
b
}
through cargo rustc -- --emit mir
, and we get this:
// WARNING: This output format is intended for human consumers only
// and is subject to change without notice. Knock yourself out.
fn too(_1: T) -> T {
debug a => _1; // in scope 0 at src/lib.rs:6:24: 6:25
let mut _0: T; // return place in scope 0 at src/lib.rs:6:33: 6:34
let _2: T; // in scope 0 at src/lib.rs:7:9: 7:10
let mut _3: bool; // in scope 0 at src/lib.rs:7:16: 7:42
let mut _4: bool; // in scope 0 at src/lib.rs:13:1: 13:2
scope 1 {
debug b => _2; // in scope 1 at src/lib.rs:7:9: 7:10
}
bb0: {
_4 = const false; // scope 0 at src/lib.rs:7:9: 7:10
_4 = const true; // scope 0 at src/lib.rs:7:9: 7:10
StorageLive(_2); // scope 0 at src/lib.rs:7:9: 7:10
StorageLive(_3); // scope 0 at src/lib.rs:7:16: 7:42
_3 = complicated_runtime_bool() -> [return: bb1, unwind: bb6]; // scope 0 at src/lib.rs:7:16: 7:42
// mir::Constant
// + span: src/lib.rs:7:16: 7:40
// + literal: Const { ty: fn() -> bool {complicated_runtime_bool}, val: Value(Scalar(<ZST>)) }
}
bb1: {
switchInt(_3) -> [false: bb2, otherwise: bb3]; // scope 0 at src/lib.rs:7:13: 11:6
}
bb2: {
_2 = <T as Default>::default() -> [return: bb9, unwind: bb6]; // scope 0 at src/lib.rs:10:9: 10:27
// mir::Constant
// + span: src/lib.rs:10:9: 10:25
// + literal: Const { ty: fn() -> T {<T as std::default::Default>::default}, val: Value(Scalar(<ZST>)) }
}
bb3: {
_4 = const false; // scope 0 at src/lib.rs:8:9: 8:10
_2 = move _1; // scope 0 at src/lib.rs:8:9: 8:10
goto -> bb4; // scope 0 at src/lib.rs:7:13: 11:6
}
bb4: {
StorageDead(_3); // scope 0 at src/lib.rs:11:6: 11:7
_0 = move _2; // scope 1 at src/lib.rs:12:5: 12:6
StorageDead(_2); // scope 0 at src/lib.rs:13:1: 13:2
switchInt(_4) -> [false: bb5, otherwise: bb8]; // scope 0 at src/lib.rs:13:1: 13:2
}
bb5: {
return; // scope 0 at src/lib.rs:13:2: 13:2
}
bb6 (cleanup): {
drop(_1) -> bb7; // scope 0 at src/lib.rs:13:1: 13:2
}
bb7 (cleanup): {
resume; // scope 0 at src/lib.rs:6:1: 13:2
}
bb8: {
drop(_1) -> bb5; // scope 0 at src/lib.rs:13:1: 13:2
}
bb9: {
goto -> bb4; // scope 0 at src/lib.rs:10:9: 10:27
}
}
fn complicated_runtime_bool() -> bool {
let mut _0: bool; // return place in scope 0 at src/lib.rs:2:38: 2:42
bb0: {
_0 = const true; // scope 0 at src/lib.rs:3:5: 3:9
return; // scope 0 at src/lib.rs:4:2: 4:2
}
}
Looks like the parameter a
got called _1
, and it got a drop flag indeed, in _4
. The move happens where _2 = move _1;
, and it updates the flag _4 = const false;
at the same time as well. Then—before returning—the flag _4
is checked in switchInt(_4) -> [false: bb5, otherwise: bb8];
, and if it’s true, drop(_1)
happens.