What happens when a stack-allocated value is boxed?
Asked Answered
E

2

8

If we have a value that is already allocated on stack, will boxing copy it to heap and then transfer ownership (that's how it works in .NET, with the exception that both copies will stay alive)? Or will the compiler be "smart" enough to allocate it directly on heap from the beginning?

struct Foo {
    x: i32,
}

fn main() {
    // a is allocated on stack?
    let a = Foo { x: 1 };

    // if a is not used, it will be optimized out
    println!("{}", a.x);

    // what happens here? will the stack allocated structure
    // be moved to heap? or was it originally allocated on heap?
    let b = Box::new(a);
}

I'm not a specialist in assembler, but this looks like it is actually allocated on stack and then moved: http://pastebin.com/8PzsgTJ1. But I need a confirmation from someone who actually knows what is happening.

Extra answered 13/4, 2015 at 22:7 Comment(0)
S
5

It would be pretty strange for this optimization to happen as you describe it. For example, in this code:

let a = Foo { x: 1 };
// operation that observes a
let b = Box::new(a);
// operation that observes b

&a and &b would be equal, which would be surprising. However, if you do something similar, but don't observe a:

#[inline(never)]
fn frobnotz() -> Box<Foo> {
    let a = Foo { x: 1 };
    Box::new(a)
}

You can see via the LLVM IR that this case was optimized:

define internal fastcc noalias dereferenceable(4) %Foo* @_ZN8frobnotz20h3dca7bc0ee8400bciaaE() unnamed_addr #0 {
entry-block:
  %0 = tail call i8* @je_mallocx(i64 4, i32 0)
  %1 = icmp eq i8* %0, null
  br i1 %1, label %then-block-106-.i.i, label %"_ZN5boxed12Box$LT$T$GT$3new20h2665038481379993400E.exit"

then-block-106-.i.i:                              ; preds = %entry-block
  tail call void @_ZN3oom20he7076b57c17ed7c6HYaE()
  unreachable

"_ZN5boxed12Box$LT$T$GT$3new20h2665038481379993400E.exit": ; preds = %entry-block
  %2 = bitcast i8* %0 to %Foo*
  %x.sroa.0.0..sroa_idx.i = bitcast i8* %0 to i32*
  store i32 1, i32* %x.sroa.0.0..sroa_idx.i, align 4
  ret %Foo* %2
}

Similarly, you can return the struct on the stack and then box it up, and there will still just be the one allocation:

You may think that this gives us terrible performance: return a value and then immediately box it up ?! Isn't this pattern the worst of both worlds? Rust is smarter than that. There is no copy in this code. main allocates enough room for the box, passes a pointer to that memory into foo as x, and then foo writes the value straight into the Box.

Susannasusannah answered 14/4, 2015 at 0:22 Comment(1)
Thank you for the detailed answer and a link. I thought that there is no need to read that "Unstable" part of the Rust book. I was wrong.Extra
C
3

As explained in the official Rust documentation here, Box<T>::new(x: T) allocates memory on the heap and then moves the argument into that memory. Accessing a after let b = Box::new(a) is a compile-time error.

Curtsey answered 13/4, 2015 at 22:24 Comment(4)
That's exactly what I wanted to know. As always, its RTFM problem :)Extra
@SergiiBogomolov: Don't feel bad about it, it takes time to learn to navigate new documentation (especially when it keeps changing).Inerrable
@SergiiBogomolov to clarify - the compiler can optimize this case (as described in my answer) if the "stack-allocated" value is immediately moved to the Box. It does not optimize if the value is used before being moved to the Box.Susannasusannah
this means there is a byte-to-byte copy from stack to heap?Question

© 2022 - 2024 — McMap. All rights reserved.