10.5 Enums and Memory Layout

Understanding enum memory representation helps with performance analysis and FFI.

10.5.1 Memory Size

An enum instance requires memory for its discriminant (tag identifying the active variant) plus enough space to hold the data of its largest variant.

// Example sizes, actual values depend on architecture and alignment
enum ExampleEnum {
    VariantA(u8), // Size = max(size(u8), size(i64), size([u8;128])) + size(disc.)
    VariantB(i64), //       (Likely 128 bytes + padding + discriminant size)
    VariantC([u8; 128]),
}

fn main() {
    // All instances of ExampleEnum have the same size, regardless of active variant.
    let size = std::mem::size_of::<ExampleEnum>();
    println!("Size of ExampleEnum: {} bytes", size); // Likely > 128

    let instance_a = ExampleEnum::VariantA(10);
    let instance_c = ExampleEnum::VariantC([0; 128]);

    //size_of_val(&instance_a) == size_of_val(&instance_c) == size_of::<ExampleEnum>()
    println!("Size of instance_a: {}", std::mem::size_of_val(&instance_a));
    println!("Size of instance_c: {}", std::mem::size_of_val(&instance_c));
}

This consistent size simplifies memory management (e.g., storing enums in arrays) but means small variants still occupy the space needed by the largest one.

10.5.2 Optimizing Memory Usage with Box

If one variant is much larger than others and less frequently used, store its data on the heap using Box (a smart pointer) to reduce the enum’s overall stack size.

// This enum's size is determined by the larger Box pointer + discriminant
enum OptimizedEnum {
    VariantA(u8),
    VariantB(i64),
    VariantC(Box<[u8; 1024]>), // Data on heap, enum holds pointer
}

// This enum's size is determined by the large array + discriminant
enum LargeEnum {
     VariantA(u8),
     VariantB(i64),
     VariantC([u8; 1024]),     // Data stored inline
}

fn main() {
    let size_optimized = std::mem::size_of::<OptimizedEnum>();
    let size_large = std::mem::size_of::<LargeEnum>();
    let size_box = std::mem::size_of::<Box<[u8; 1024]>>(); // Size of a pointer

    println!("Size of OptimizedEnum: {} bytes", size_optimized); // Smaller
    println!("Size of LargeEnum:     {} bytes", size_large); // Much larger (>= 1024)
    println!("Size of Box pointer:   {} bytes", size_box);   // e.g., 8 on 64-bit

    // Create an instance with boxed data
    let large_data = Box::new([0u8; 1024]);
    let instance = OptimizedEnum::VariantC(large_data);
    // 'instance' (on stack) is small; the 1024 bytes are on the heap.
    println!("Size of instance value: {}", std::mem::size_of_val(&instance));
}
  • Box<T>: Stores T on the heap, keeping only a pointer on the stack. Size of Box<T> is the pointer size.
  • Trade-off: Reduces stack size but adds heap allocation cost and one level of indirection for data access. Best when large variants are rare or memory savings are critical (e.g., in large collections).

Box and smart pointers are detailed in Chapter 19.

Note on Niche Optimization: Rust can optimize layout. For instance, Option<Box<T>> usually occupies the same space as Box<T>, using the null pointer state for the None discriminant. Option<&T> also uses the null niche. This avoids overhead for optional pointers/references.*