Why does stack pinning (e.g. `pin_mut!()`) allocate memory?

32 points Patryk27

tl;dr

This:

#![feature(bench_black_box)]

use core::hint::black_box;

#[inline(never)]
pub fn foo<T>(mut val: T) {
    loop {
        black_box(&mut val);
    }
}

pub fn bar() {
    foo([0u8; 1024]);
}

... compiles to:

define ... {
start:
  %0 = alloca [1024 x i8]*, align 8
  %1 = bitcast [1024 x i8]** %0 to i8*
  
  /* ... */
}

... but pinning val to the stack:

#[inline(never)]
pub fn foo<T>(val: T) {
    let mut val = val;

    loop {
        black_box(&mut val);
    }
}

... causes rustc to emit a memcpy:

define ... {
start:
  %0 = alloca [1024 x i8]*, align 8
  %val1 = alloca [1024 x i8], align 1
  %1 = getelementptr inbounds [1024 x i8], [1024 x i8]* %val1, i64 0, i64 0
  %2 = getelementptr inbounds [1024 x i8], [1024 x i8]* %val, i64 0, i64 0
  call void @llvm.memcpy.p0i8.p0i8.i64(...)
  %3 = bitcast [1024 x i8]** %0 to i8*
  br label %bb1
}

... which, to me, seems overzealous, since the value already lives on the stack; this copy doesn't seem to accomplish anything besides causing the function to eat more stack space.

Now, I understand that this copying might be necessary sometimes (I guess if I were to Box or Rc that value?), but in there it seems futile - can that be just a missed optimization opportunity or there's something fundamental I'm not seeing? :-)

Obligatory: tested on --release, current nightly & from a few months ago.

Context:

I'm implementing an async executor for an AVR Rust project I'm doing:

impl Runtime {
    /* ... */

    pub fn enter(self, main: impl Future<Output = !>) -> ! {
        /* ... */
        
        pin_mut!(main);

        loop {
            match main.as_mut().poll(&mut context) {
                Poll::Ready(_) => {
                    // Unreachable
                }

                Poll::Pending => {
                    //
                }
            }
        }
    }
}

... and I saw that when it's given a "large" future, it triggers a CPU-reset.

After a while I've nailed the reset down to a stack overflow (after all, there's just 2k of RAM), but measuring the future's size (~800 bytes) suggested that everything should be working correctly - I ain't using that much memory.

Eventually I've changed pin_mut!() to:

impl Runtime {
    /* ... */

    pub fn enter(self, mut main: impl Future<Output = !>) -> ! {
        /* ... */
        
        let mut main = unsafe { Pin::new_unchecked(&mut main) };

        /* ... */
    }
}

... which caused my application to work correctly, suggesting to me (which I also confirmed with gdb) that pin_mut!() triggers a memcpy.