A bool only has 2 possibilities: true or false. So a tuple of 2 bools only has 4 possibilities, well under 256 maximum possibilities that 8 bits can afford.
Rust also has size optimization for NonZero and reference types.
So why doesn't bool benefit from such optimization?
The reason is that you can produce an &/&mut bool pointing to either of the two fields, which means that the internal layout of each bool has to be the canonical one— The consumers of those references don't know that an alternative memory layout might be in play.
If you put a bool into an enum with a bunch of unit variants, the same niche optimization as you get for NonZero will kick in and store the discriminant as unrepresentable values with an overall size of 1.
In Rust (like in C), a bool is defined to occupy 1 full byte (8 bits) in memory. That’s by design, not because it needs all 256 values, but because:
It guarantees ABI compatibility with C (_Bool in C also has size 1).
It simplifies pointer alignment and memory access (you can always load/store a bool as a full byte without having to do bit twiddling).
CPUs usually have instructions that load/store a whole byte, but not necessarily 1 bit.
It allows reference only one field of a struct.
So bool isn’t optimized down to just 1 bit individually.
For things like Option<NonZeroU8>, Rust uses niche optimizations.
Example:
A NonZeroU8 cannot ever be 0.
That means 0 is “free real estate” for Option<NonZeroU8> to use as a special discriminant for None.
So Option<NonZeroU8> is still only 1 byte, not 2.
This works because there’s an unused bit pattern.
You might expect: bool has 254 unused bit patterns (anything not 0x00 or 0x01), so surely Option<bool> could pack into 1 byte?
But Rust does not currently exploit this niche for bool. Instead, Option<bool> is 2 bytes (1 for the discriminant, 1 for the bool), at least on stable today.
EDIT: This is not true anymore. Option<bool>is optimized.
If you want to pack bool together to use less space, you can use the bitvec crate.
It's essentially because of alignment. bool's alignment is 1 byte = 8 bits. For the same reason, ((u64, u8), (u64, u8)) is 32 bytes and not 18 and not even 24 bytes, since u64's alignment is 8 bytes.
Minimum alignment is 8 bits because hardware is built with byte-alignment in mind: all memory addresses are aligned to bytes.