Option Layout and Niche Optimization


title: “NonNull Niche Optimization in Rust” meta_description: “How Option<NonNull> achieves zero-overhead representation using null pointer optimization.” keywords: [“rust niche optimization”, “option nonnull”, “zero cost abstraction”, “null pointer optimization”]

🎯 Reinventing from Scratch: NonNull<T>

Chapter 5 — “Option Layout and Niche Optimization”

“The null pointer bit pattern becomes the None variant — for free!”


✨ 5.1. The Magic

use std::mem::size_of;
use std::ptr::NonNull;

assert_eq!(
    size_of::<Option<NonNull<u8>>>(),
    size_of::<*mut u8>()
);

// Option<NonNull<T>> is the SAME SIZE as a raw pointer!

How is this possible?


🎭 5.2. Understanding Niche Optimization

Normally, Option<T> needs a discriminant tag:

enum Option<T> {
    Some(T),  // Tag: 1
    None,     // Tag: 0
}

// Layout: [tag: u8, value: T, padding]
// Size: 1 + size_of::<T>() + padding

But for NonNull<T>:

enum Option<NonNull<T>> {
    Some(NonNull<T>),  // Non-null pointer
    None,              // Null pointer
}

// The null pointer value IS the None tag!
// No extra byte needed!

🔍 5.3. How It Works

The compiler knows:

  • NonNull<T> can never be null (by invariant)
  • Pointers have a “niche” — the null value is unused
  • Option can use that niche for the None variant!

Result: Zero-overhead Option!


📊 5.4. Visual Representation

Regular Option

Option<Box<i32>>:
┌─────────┬──────────┐
│ Tag: 1B │ Ptr: 8B  │  = 16 bytes (with padding)
└─────────┴──────────┘

None:  Tag = 0, Ptr = garbage
Some:  Tag = 1, Ptr = valid address

Niche-Optimized Option

Option<NonNull<i32>>:
┌──────────┐
│ Ptr: 8B  │  = 8 bytes
└──────────┘

None:  Ptr = 0x0000000000000000 (null)
Some:  Ptr = 0x00007fff... (non-null)

🎨 5.5. Other Types with Niche Optimization

Type Niche Optimization
NonNull<T> Null value Option<NonNull<T>>
&T Null value Option<&T>
&mut T Null value Option<&mut T>
NonZeroU32 Zero value Option<NonZeroU32>
bool Values 2-255 Option<bool> is 1 byte!

🧪 5.6. Testing Niche Optimization

#[test]
fn test_niche_optimization() {
    use std::mem::size_of;
    use std::ptr::NonNull;
    
    // NonNull
    assert_eq!(
        size_of::<Option<NonNull<i32>>>(),
        size_of::<*mut i32>()
    );
    
    // References
    assert_eq!(
        size_of::<Option<&i32>>(),
        size_of::<*const i32>()
    );
    
    // Multiple Options!
    assert_eq!(
        size_of::<Option<Option<NonNull<i32>>>>(),
        size_of::<*mut i32>()
    );
    // Still the same size!
}

🎯 5.7. Practical Benefits

1. Memory-Efficient Data Structures

struct Node<T> {
    value: T,
    next: Option<NonNull<Node<T>>>,  // Only 8 bytes!
}

// Linked list node is as small as possible

2. Intrusive Collections

struct IntrusiveListNode {
    prev: Option<NonNull<IntrusiveListNode>>,  // 8 bytes
    next: Option<NonNull<IntrusiveListNode>>,  // 8 bytes
    // No extra tag bytes!
}

3. Optional Pointers Everywhere

struct MyVec<T> {
    ptr: NonNull<T>,        // 8 bytes
    parent: Option<NonNull<ParentNode>>,  // 8 bytes (not 16!)
}

🧠 5.8. Chapter Takeaways

  • Niche = Unused bit pattern in a type
  • Option<NonNull<T>> = Same size as raw pointer
  • Null pointer = None discriminant (free!)
  • Zero-cost abstraction = Better safety, no performance cost

🚀 5.9. What’s Next

Next: Chapter 6 — Building on Box and RawVec

See how NonNull improves real data structures:

  • Refactor Box to use NonNull
  • Build RawVec with NonNull
  • Clearer invariants and safer APIs

💡 Exercises

  1. Verify size of Option<NonNull> for different T
  2. Build a linked list using Option<NonNull>
  3. Measure memory savings compared to Option<*mut T>
  4. Implement your own niche-optimized type
  5. Test that None is represented as null pointer

Next: Chapter 6 — Building on Box and RawVec 🏗️