Introduction
Rust's default struct layout reorders fields for optimal alignment and minimal padding. Understanding how the compiler lays out your types lets you minimize memory usage and improve cache utilization.
Key Concepts
Default Layout
Rust is free to reorder fields to minimize padding:
ruststruct Example { a: u8, // 1 byte b: u64, // 8 bytes c: u16, // 2 bytes } // Rust reorders to: b(8) + c(2) + a(1) + padding(5) = 16 bytes
#[repr(C)]: C-Compatible Layout
rust#[repr(C)] struct CExample { a: u8, // offset 0, +7 padding b: u64, // offset 8 c: u16, // offset 16, +6 padding } // Total: 24 bytes (worse!)
#[repr(packed)]: No Padding
rust#[repr(packed)] struct Packed { a: u8, b: u64, c: u16, } // Total: 11 bytes, but unaligned access is slow
Real World Context
Network protocols and file formats use repr(C) for FFI compatibility. Game engines carefully control struct sizes to fit more entities in cache. Serialization libraries rely on predictable layouts.
Deep Dive
Checking Size and Alignment
rustuse std::mem::{size_of, align_of}; println!("Size: {}", size_of::<Example>()); // 16 println!("Align: {}", align_of::<Example>()); // 8
Null Pointer Optimization
Rust uses niche optimization: Option<&T> is the same size as &T because null is used for None:
rustassert_eq!(size_of::<&u64>(), 8); assert_eq!(size_of::<Option<&u64>>(), 8); // Free Option! // Also works for Box, NonNull, NonZeroU64, etc. assert_eq!(size_of::<Option<Box<u64>>>(), 8);
Optimizing Enum Size
rust// Large variant makes the whole enum large enum Message { Quit, Text(String), // 24 bytes Data([u8; 1024]), // 1024 bytes! } // size_of::<Message>() = 1032 (discriminant + largest variant) // Fix: Box the large variant enum MessageOptimized { Quit, Text(String), Data(Box<[u8; 1024]>), // 8 bytes (pointer) } // size_of::<MessageOptimized>() = 32
Common Pitfalls
- Using
#[repr(packed)]without understanding the performance cost — unaligned access is slow on most architectures and references to packed fields are unsafe. - Assuming field order in default layout — Rust provides no guarantees.
- Creating enums with one disproportionately large variant — Box it instead.
Best Practices
- Let the compiler optimize layout (default repr) unless you need FFI compatibility.
- Use
size_ofandalign_ofto verify your assumptions. - Box large enum variants to keep the enum small.
- Use
NonZeroU*types when zero is not a valid value to enable niche optimization.
Summary
Rust reorders struct fields to minimize padding by default. Use repr(C) for FFI, avoid repr(packed) unless necessary, leverage null pointer optimization with Option, and Box large enum variants. Always verify sizes with std::mem::size_of.
Code Examples
use std::mem::size_of;
use std::num::NonZeroU64;
// Null pointer optimization
assert_eq!(size_of::<&u64>(), 8);
assert_eq!(size_of::<Option<&u64>>(), 8); // Same size!
assert_eq!(size_of::<Box<u64>>(), 8);
assert_eq!(size_of::<Option<Box<u64>>>(), 8); // Same size!
assert_eq!(size_of::<Option<NonZeroU64>>(), 8); // Same size!
// Enum size optimization
enum Large {
A,
B([u8; 1024]),
}
enum Small {
A,
B(Box<[u8; 1024]>),
}
println!("Large: {} bytes", size_of::<Large>()); // 1028
println!("Small: {} bytes", size_of::<Small>()); // 16