What happens to `Moved` values?

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.)

9 Likes

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.

Drop Flags - The Rustonomicon

5 Likes

I already thought something g like that would happen, but I thought it served as an example :slight_smile:

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).

2 Likes

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.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.