Defer memcpy when passing types that is !Copy until it is actually moved to another variable

the rust compiler makes many unnecessary calls to memcpy to move values around when calling functions and methods which accepts a value parameter while the parameter might not be used by the function but forwarded to another one so copying it over and over is useless here. For this to work the type must be !Copy so consumed by move and any changes by the calleee will not be observed by the caller and since rust disallows moving while holding a reference or reference a consumed object it is mark the object as consumed and just pass its address to the callee. Can't the compiler optimize this anyway ? the compiler seems to be able to optimize this case only when optimization is at highest level and when the calls are inlined but not all functions can be inlined from other modules or the method size may be too large to inline.

example code:

pub struct SomeStruct {
    pub data: [u8; 256],
}

#[derive(Default)]
pub struct OtherStruct {
    pub st: SomeStruct,
}

impl Default for SomeStruct {
    fn default() -> Self
    {
        Self {
            data: [0_u8; 256]
        }
    }
}

impl OtherStruct {
    // pass address of st instead of copied st
    fn new(st: SomeStruct) -> Self {
        Self {
            st: st // only memcpy here
        }
    }
}

pub fn make_other_struct(st: SomeStruct) -> OtherStruct {
    // pass address of st instead of copied st
    OtherStruct::new(st)
}

extern {
    fn extn_use(ot: OtherStruct);
}

pub fn main() {
    let st = SomeStruct::default();
    // pass address of st instead of address of copied st
    let ot = make_other_struct(st);
    // pass address of ot instead of address of copied ot
    drop(ot); // calls Drop::drop() on the address of ot which is safe since ot can't be referenced afterwards
}

This should already be the case as SomeStruct doesn't fit into a register and thus is passed by-ref.

This is LLVM not knowing that it is fine to clobber the by-ref st value. MIR copy-propagation should be able to optimize this away, but I believe it is broken at the moment and also rather slow.

This should already be the case as SomeStruct doesn't fit into a register and thus is passed by-ref

In the mean time rust allocates space on the stack for a new SomeStruct then memcpy the original one into this allocated memory and then pass a reference to the newly created objected to the method

This is LLVM not knowing that it is fine to clobber the by-ref st value. MIR copy-propagation should be able to optimize this away, but I believe it is broken at the moment and also rather slow.

AFAIK rust does not tell LLVM that it is passing by value but by reference to the copied value

Also c++ have an rvalue references that is used like this: the reference is moved around until it is moved from explicitly which is where I got this idea

See Do move forwarding on MIR · Issue #32966 · rust-lang/rust · GitHub -- getting some kind of useful NRVO in Rust has a long-lasting wish.

It's also painful for wrapper types, like Using ManuallyDrop causes allocas and memcpys that LLVM cannot remove · Issue #79914 · rust-lang/rust · GitHub.

1 Like

The rough equivalent in rust is what's known colloquially as &move or &own.

But yes, this is more generally known as move elision. There are ongoing efforts to make the compiler smarter here.

However, this optimization is generally not currently allowed. A move operation is currently defined/implemented as leaving the bytes as-is in the moved-from place, and reading those bits with unsafe will give you back the bytes left in the place.

(This is unlike C++ move ctors, which do write to the moved-from place, and the moved from place is still destroyed.)

This means that Rust's "destructive moves" aren't quite destructive enough. This is currently done this way because this means there is nothing different between a move and a copy beyond whether the moved-from place gets dropped.

It's an open question whether moves should deinitialize the moved-from place (make reads of it return an undefined value, allowing the compiler to clobber it arbitrarily). AIUI this doesn't matter when moving out of a named stack place (as the move will invalidate any existing pointers and prevent the place from being accessed, providing the same benefit), but it can matter with other combinations of features which allow you to name a place after moving from it in order to unsafely read from it.

1 Like