Table of content

Introduction

If you have ever attempted to check the size of Option<i32> and i32 in Rust, you might have noticed that the sizes are 8 and 4, respectively. A bird-eye view of this might conclude that this will always apply for all case Option<T> and T respectively, but that isn’t always true.

Examples

For the below examples, you will discover that all NonZero* types have a consistent size with their equivalent Option<NonZero*>. The reason is how these underlying types are laid out in memory compared to their nearly equivalent primitive type.

T size_of::<T>() size_of::<Option<T>()
i32 4 8
usize 8 16
NonZeroUsize 8 8
NonZeroI32 4 4

NonZero types in Rust

A look at docs.rs for NonZeroI32 and NonZeroUsize refers to them as “an integer that is known not to equal zero. This enables some memory layout optimization”.

The keyword here is optimization; for a type to be optimized in memory, it is important to have more information about the type, such as its size and alignment (more on this later). Based on our information about a type, we can further optimize how it’ll be stored in memory.

For a type to be stored in memory, you must also consider how it can be retrieved efficiently. Suppose the size and alignment of a type are the values that can be predicted. In that case, we can deterministically retrieve its value since we can always determine the address of the value in memory.

Digging Deeper

Since Option<T> is an enum variant, exploring how enums are represented in memory will prove helpful in understanding the size it wraps.

Representing Rust’s enum in C

It’s worth noting that C can't directly represent complex Rust enums; hence, there is a need for a workaround. For example, Rust’s Option<T> has the following declaration:

#[repr(C)]
enum Option<T> {
    Some(T),
    None
}

At compile time, an Option<i32> will get expanded into the following C-compatible code: