Using the following snippet
use std::mem;
fn main() {
println!("size Option(bool): {} ({})", mem::size_of::<Option<bool>>(), mem::size_of::<bool>());
println!("size Option(u8): {} ({})", mem::size_of::<Option<u8>>(), mem::size_of::<u8>());
println!("size Option(u16): {} ({})", mem::size_of::<Option<u16>>(), mem::size_of::<u16>());
println!("size Option(u32): {} ({})", mem::size_of::<Option<u32>>(), mem::size_of::<u32>());
println!("size Option(u64): {} ({})", mem::size_of::<Option<u64>>(), mem::size_of::<u64>());
println!("size Option(u128): {} ({})", mem::size_of::<Option<u128>>(), mem::size_of::<u128>())
}
I see on my 64-bits machine:
size Option(bool): 1 (1)
size Option(u8): 2 (1)
size Option(u16): 4 (2)
size Option(u32): 8 (4)
size Option(u64): 16 (8)
size Option(u128): 24 (16)
So the overhead is not constant and goes up to 8
bytes. I wonder why the overhead is not just one byte to store the tag? I also wonder what representation is chosen by the compiler?
The Rust Reference on type layouts comes into play here:
[...] The size of a value is always a multiple of its alignment. [...]
The only data layout guarantees made by [the default] representation are those required for soundness. They are: [...]
- The alignment of the type is at least the maximum alignment of its fields.
So the size of Option<T>
must be rounded up to the nearest alignment of T
, even if only one byte (or even one bit) is used to store the information of "value is present".
The exception is types that allow for "null pointer optimization", where Option<T>
has the same size of T
because it can represent None
by using one of the invalid states of T
. For example, bool
only has two states, so the compiler will optimize and use one of the remaining 254 1-byte states to represent None
for Option<bool>
. This works for bool
, &U
, &mut U
, fn
, Box<U>
, NonZero*
and NonNull<U>
.