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
Optioncan use that niche for theNonevariant!
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
- Verify size of Option<NonNull
> for different T - Build a linked list using Option<NonNull
> - Measure memory savings compared to Option<*mut T>
- Implement your own niche-optimized type
- Test that None is represented as null pointer
Next: Chapter 6 — Building on Box and RawVec 🏗️