Box question: heap or stack?

As noted, the Box allocate on heap for value.
But should we need to create a value on stack and then copy to the box?
For example,

struct Foo {
    val: i32,
}

impl Drop for Foo {
    fn drop(&mut self) {
        println!("drop me");
    }
}
fn main() {
    let a = Foo { val: 1 };
    let aa = Box::new(a);
    println!("{}", aa.val);
}

a is Foo type and allocated on stack.
Then box it, which move a to the heap, correct?

So, how to create value in box directly? Or the compiler could check this case and optimize it (I check the Box::new, it uses box t. What's the black-magic box?)

This is to the best of my knowledge, but may be wrong:

Yes.

Yes.

  1. I don't know how the compiler optimizes this.

  2. How would you "create value in box directly" ? This sounds like doing a malloc then assigning to the fields. However, in Rust, I'm not sure if it's safe / defined behaviour to have uninitialized fields/variables.

  3. I might be that you have a different model for "create value in box directly" in mind.

1 Like

AFAIK right now compiler usually does not optimize it, which can cause stack overflows. box is a keyword introduced by RFC 0809. If you want to create value directly on the heap, I think it will be better to use Box::new_uninit followed by assume_init after initialization is complete.

Or, on stable Rust, use the ::copyless crate:

use ::copyless::BoxHelper;

Box::alloc().init(Foo { val: 1 });
2 Likes

Still confused.
Foo { val: 1 } is already created on stack, and then move to init.
So what does copyless do?

A distinction must be drawn between the abstractions of the language and how these may be implemented.

Yes, every function parameter is first a "local", and yes, most locals are implemented as stack variables. But when the optimizer kicks in, it can inline / reorganize most of this stuff. So, for instance, both your local a, or my anonymous local can be fed directly to the functions consuming it, without needing to construct the value in the stack beforehand.

However, the optimizer can only do changes that do not affect the semantics of the program. And one very hidden semantic of Box::new(<expr>) is that:

  1. A local <expr> is created;

  2. An allocation to hold it is attempted;

  3. Then the value is moved to the heap,

But if 2. fails (e.g., out of memory), then it may panic, so this local must be dropped.

  • And even it instead of panic!-king it aborted, then since the creation of <expr> might have had side-effects it needs to happen whatever the result of the allocation.

This prevents an optimizer to inline / move the creation of the struct directly into its backing (heap) allocation.

The ::copyless idiom, Box::alloc().init(<expr>), expresses the following semantics:

  1. Box::alloc()

    • An allocation is attempted;

      • if it fails, then panic / abort; but since <expr> does not exist yet, there is no interaction with that whatsoever.
  2. .init(<expr>)

    • Create a local <expr>;

    • Move that value into the heap. // <- this cannot fail

With these semantics the optimizer is allowed to inline the creation of <expr> directly into the heap.

6 Likes

Move to heap, then it's still a copy?

I give a try.

#![feature(new_uninit)]

#[derive(Debug)]
struct Foo {
    val: i32,
}

impl Drop for Foo {
    fn drop(&mut self) {
        println!("drop me");
    }
}
fn main() {
    let mut x = Box::<Foo>::new_uninit();
    let foo = unsafe { &mut *x.as_mut_ptr() };
    foo.val = 3;
    println!("{:?}", foo);
}

I found drop is never be called, why?

MaybeUninit can't know if its contents are initialized, so it doesn't drop the contained object (as dropping garbage memory would be undefined behavior). If you look at its definition, you can see that it holds the wrapped value as a ManuallyDrop<T>.

Allocation failures trigger an abort, not a panic.

impl<T> BoxAllocation<T> {
    /// Consumes self and writes the given value into the allocation.
    #[inline(always)] // if this does not get inlined then copying happens
    pub fn init(self, value: T) -> Box<T> {

The inline attribute is the key to avoid copy?

1 Like

copyless can't guarantee that the copy will be elided (no one but LLVM can), but it makes it really easy for LLVM to elide the copy.

What you want isn't currently supported.

Note also the restriction on as_mut_ptr

They may panic in the future, so I just stay open-minded to both possibilities now.

This is Undefined Behavior; since the pattern involving MaybeUninit can be misused, I suggest favoring ::copyless, which basically wraps this pattern in a non-unsafe API.

As @Yato said, these optimizations are hard to guarantee from a library author perspective, but by hinting the optimization to the compiler it is very likely they will happen. And indeed, the #[inline(always)] is such a hint.

I don't think so, given that panic may allocate, but I agree that it the quoted code is UB because references to uninit are UB.

On another note: The copyless interface could be supported by std directly via something like fn Box:::<MaybeUninit<T>>::write(T) -> Box<T>

1 Like

That was the initial stance, but there seem to have been several arguments going in the other direction. In fact, the RFC 2116, which features a Cargo.toml flag to enable unwinding on OOM was accepted.

1 Like

Interesting, thanks for the link

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.