Smallbox alternative

Hello. I want something that essentially does the same thing as smallbox. ie. exposes a type with a fixed size and a little bit of local storage, and will allocate if the contained T is bigger than the fixed storage allotment.

Unlike smallbox, I don't need to support DSTs, and also I don't want to spend the extra 8 bytes on an additional internal pointer, like smallbox does.

Also, it looks like smallbox creates a self-referential pointer in the local case (the code here: smallbox/src/smallbox.rs at 5f33267ec0dbbb399f0556c1174f71cfb38d0543 · andylokandy/smallbox · GitHub) which didn't seem like it would work if the structure is moved. But it's possible I misunderstood the smallbox code.

First question is: is there a reliable, well-tested crate for this that already exists? I didn't see one and it looked like smallbox has the most users.

If not, are there any safety / soundness issues with my solution here?

// #[repr(align(64))]
//TODO, find a way to tell the compiler to set alignment to match the minimum of T and Box<T>
union LocalOrHeap<T> {
    heap: ManuallyDrop<Box<T>>,
    local: MaybeUninit<TStorage>,
}
type TStorage = [u8; 16];

impl<T> LocalOrHeap<T> {
    pub fn new(val: T) -> Self {
        //TODO, see above, we want to tell the compiler to align this, rather than panicking if it's unaligned 
        if core::mem::align_of::<Self>() < core::mem::align_of::<T>() {
            panic!("BoxOrValue<T> must have an alignment that is the same or coarser than T");
        }
        if core::mem::size_of::<T>() > core::mem::size_of::<TStorage>() {
            Self{ heap: ManuallyDrop::new(Box::new(val))}
        } else {
            let mut storage = MaybeUninit::<TStorage>::uninit();
            let size = core::mem::size_of::<T>();
            // SAFETY: We know we won't overwrite storage, and we also know storage
            // will be aligned at least as coarsely as T
            unsafe{ core::ptr::copy_nonoverlapping(&val as *const T as *const u8, storage.as_mut_ptr() as *mut u8, size); }
            core::mem::forget(val);
            Self{ local: storage }
        }
    }
}

impl<T> Drop for LocalOrHeap<T> {
    fn drop(&mut self) {
        if core::mem::size_of::<T>() > core::mem::size_of::<TStorage>() {
            // SAFETY: We know we have a `heap` because of the size of T
            unsafe{ ManuallyDrop::drop(&mut self.heap) };
        } else {
            // SAFETY: We know we have a `local` because of the size of T
            unsafe{ core::ptr::drop_in_place::<T>((self.local.as_mut_ptr()).cast()); }
        }
    }
}

impl<T> AsRef<T> for LocalOrHeap<T> {
    fn as_ref(&self) -> &T {
        if core::mem::size_of::<T>() > core::mem::size_of::<TStorage>() {
            // SAFETY: We know we have a `heap` because of the size of T
            unsafe{ &self.heap }
        } else {
            // SAFETY: We know we have a `local` because of the size of T
            unsafe{ &*self.local.as_ptr().cast() }
        }
    }
}

Finally, is there a way I can specify "Align my type to the maximum of the alignments of these other types"?

Any feedback is appreciated. Thank you.

This is how the alignment of compound types, including union, is calculated, so you just need to add a member that has the alignment of T, for example:

union LocalOrHeap<T> {
    heap: ManuallyDrop<Box<T>>,
    local: MaybeUninit<TStorage>,
    _align: ManuallyDrop<[T;0]>,
}

I don’t see anything wrong with the concept, but I haven’t read the implementation closely enough to know if it has any issues.

1 Like

A few comments:

Why do you want to pay the wasted space of local if the size of T and therefore the potential heap allocation is statically known?

The size has to be a multiple of the alignment. If size_of::<T>() <= 16 an alignment of 16 is enough. Type layout - The Rust Reference

What is your usecase? Do you want to select a suitable storage in generic code?

2 Likes

What that code in smallbox is doing is creating a (possibly-fat) pointer with a NULL address and identical metadata to the original pointer, not creating anything self-referential.

1 Like

Why do you want to pay the wasted space of local if the size of T and therefore the potential heap allocation is statically known?

I'm including the LocalOrHeap structure in another struct that I'm laying out very intentionally. So if the size were smaller, it would just lead to me putting padding there anyway.

What is your usecase? Do you want to select a suitable storage in generic code?

Yeah exactly. I want the code to work the same regardless of the size of T, but the LocalOrHeap struct to remain a fixed constant size. If that size is enough to swallow T then just use that memory. Otherwise make a box.

1 Like

I published it. Thanks for having a look.

https://crates.io/crates/local-or-heap

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.