In this lesson, you will learn how to shrink the memory footprint of your data structures by utilizing bit-packing and Rust’s "niche" optimization system.
The Cost of Padding
When you define a struct, Rust aligns its fields to specific memory boundaries to ensure the CPU can access them efficiently. Often, this results in "padding"—invisible bytes of wasted space inserted between fields to satisfy alignment requirements. Furthermore, types like bool or u8 consume an entire byte, even though they only need a fraction of that space.
Bit-Packing
If you have a group of small flags, you don't need a bool for each one (which costs 1 byte per flag). Instead, you can "pack" them into a single integer. By using bitwise operators, you can store eight boolean flags inside a single u8.
struct Flags {
// We use a single byte to store 8 individual boolean states
bits: u8,
}
impl Flags {
fn set_active(&mut self) { self.bits |= 1 << 0; }
fn is_active(&self) -> bool { (self.bits & 1 << 0) != 0 }
}
Leveraging Niches
Rust has a clever feature called "niches." A niche is a bit pattern in a type that is invalid. For example, a NonZeroU32 cannot be 0. Rust realizes that 0 is an unused state, so it uses that "niche" to store the None variant of an Option<NonZeroU32> without increasing the size of the structure.
Normally, an Option<u32> takes 8 bytes (4 for the u32 and 4 for the discriminant tag). However, Option<NonZeroU32> takes only 4 bytes because it "steals" the 0 bit pattern to represent None.
Why this matters
By reordering fields to minimize padding and using types that exploit niches, you can significantly reduce the memory footprint of your structs. When you are working with millions of objects in memory, these small savings prevent cache misses and reduce the overall pressure on your system's RAM.
Key Takeaway: By packing booleans into bitfields and choosing types that allow Rust to hide metadata in "niche" bit patterns, you can drastically reduce struct size and improve cache locality.