Why does Box<[T]> need 16 bytes in memory, but a referenced slice needs only 8? (on x64 machine)

Box<T> is basically *const T (Actually it's a newtype around Unique<T>, which itself is a NonNull<T> with PhantomData<T> (for dropck), but let's stick to *const T for simplicity).

A pointer in Rust normally has the same size as size_of::<usize>() except when T is a dynamically sized type (DST). Currently, a Box<DST> is 2 * size_of::<usize>() in size (the exact representation is not stable at the time of writing). A pointer to a DST is called FatPtr.

Currently, there are two kinds of DSTs: Slices and traits. A FatPtr to a slice is defined like this:

#[repr(C)]
struct FatPtr<T> {
    data: *const T,
    len: usize,
}

Note: For a trait pointer, len is replaced by a pointer to the vtable.

With those information, your question can be answered:

  • Box<i8>: i8 is a sized type => basically the same as *const i8 => 8 bytes in size (with 64 bit pointer width)
  • Box<[i8]>: [i8] is a DST => basically the same as FatPtr<i8> => 16 bytes in size (with 64 bit pointer width)
  • Box<&[i8]>: &[i8] is not a DST. It's basically the same as *const FatPtr<i8> => 8 bytes in size (with 64 bit pointer width)

The size of a reference depends on the "sizedness" of the referenced type:

  • A reference to a sized type is a single pointer to the memory address.
  • A reference to an unsized type is a pointer to the memory and the size of the pointed datum. That's what is called a fat pointer:

    #[repr(C)]
    struct FatPtr<T> {
        data: *const T,
        len: usize,
    }
    

A Box is a special kind of pointer that points to the heap, but it is still a pointer.

Knowing that, you understand that:

  • Box<i8> is 8 bytes because i8 is sized,
  • Box<&[i8]> is 8 bytes because a reference is sized,
  • Box<[i8]> is 16 bytes because a slice is unsized.

Tags:

Rust