Memory Layout and Representation

Fat Pointers

A fat pointer (also called a wide pointer) is a two-word pointer (2 * size_of::<usize>() bytes—16 bytes on 64-bit targets, 8 bytes on 32-bit targets) that pairs a memory address with a second word of runtime metadata required to access a dynamically sized type (DST).

Thin Pointers vs. Fat Pointers

A pointer to a Sized type (&u64, &Vec<u8>, Box<[u8; 4]>) is a thin pointer: a single usize word holding only the target memory address, because the pointee's size and method implementations are known at compile time.

When a type is unsized ([T], str, dyn Trait, or a struct whose final field is unsized), any reference or smart pointer to that type (&T, &mut T, Box<T>, Rc<T>, Arc<T>, *const T, *mut T) automatically widens to carry two words:

Thin Pointer: &[u32; 3] (8 bytes on 64-bit)
+------------------+
| data_ptr (8 B) --+------> [ 10 | 20 | 30 ]
+------------------+

Slice Fat Pointer: &[u32] (16 bytes on 64-bit)
+------------------+------------------+
| data_ptr (8 B)   | len = 3 (8 B)    |
+--------+---------+------------------+
         |
         +----------------> [ 10 | 20 | 30 ]

Trait-Object Fat Pointer: &dyn Draw (16 bytes on 64-bit)
+------------------+------------------+
| data_ptr (8 B)   | vtable_ptr (8 B) |
+--------+---------+--------+---------+
         |                  |
         v                  v
  Concrete Value     +--------------------+
                     | drop_in_place fn   |
                     | size (usize)       |
                     | align (usize)      |
                     | Draw::draw fn ptr  |
                     +--------------------+

Comparing Thin and Fat Pointer Sizes

Pointer Type Pointee Kind Size (64-bit) First Word Second Word
&u32, Box<u32>, *const u32 Sized 8 bytes Data address None
&Vec<u8>, &String Sized container 8 bytes Address of stack header None
&[u32], Box<[u32]>, *const [u32] Slice ([T]) 16 bytes Address of first element Element count (usize)
&str, Box<str>, Arc<str> String slice (str) 16 bytes Address of first UTF-8 byte Byte length (usize)
&dyn Trait, Box<dyn Trait> Trait object 16 bytes Address of concrete instance Static vtable address
trait Summary {
    fn summarize(&self) -> usize;
}

impl Summary for u64 {
    fn summarize(&self) -> usize {
        *self as usize
    }
}

fn main() {
    println!("&u64:         {} bytes", size_of::<&u64>());
    println!("&[u64; 4]:    {} bytes", size_of::<&[u64; 4]>());
    println!("&[u64]:       {} bytes", size_of::<&[u64]>());
    println!("Box<[u64]>:   {} bytes", size_of::<Box<[u64]>>());
    println!("&str:         {} bytes", size_of::<&str>());
    println!("&dyn Summary: {} bytes", size_of::<&dyn Summary>());
}
&u64:         8 bytes
&[u64; 4]:    8 bytes
&[u64]:       16 bytes
Box<[u64]>:   16 bytes
&str:         16 bytes
&dyn Summary: 16 bytes

Converting Between [T; N], Vec<T>, and Box<[T]>

A Vec<T> stores three words on the stack (ptr, len, cap = 24 bytes on 64-bit). Once a vector's length no longer needs to grow, calling Vec::into_boxed_slice() drops any excess capacity and shrinks the stack handle to a two-word Box<[T]> fat pointer (16 bytes).

fn main() {
    let mut vec = Vec::with_capacity(10);
    vec.extend_from_slice(&[1_u32, 2, 3]);
    println!("Vec<u32> stack size:   {} bytes (cap = {})", size_of_val(&vec), vec.capacity());

    let boxed_slice: Box<[u32]> = vec.into_boxed_slice();
    println!("Box<[u32]> stack size: {} bytes (len = {})", size_of_val(&boxed_slice), boxed_slice.len());
}
Vec<u32> stack size:   24 bytes (cap = 10)
Box<[u32]> stack size: 16 bytes (len = 3)