Is None allocating heap memory?

Hi!

In most programming languages,null just means there's no data and it won't allocating memory.But is None the same as null?

None does not allocate any heap memory. I'm not overly versed in the memory representation of enums in Rust (i.e. what guarantees Rust has concerning niche optimization and memory layout of Option<T> vs. T), but if I check Godbolt, Option::<i32>::None is just a 64-bit 0 on the stack.

The layout of None is not the same as a the layout of a null pointer, as std::mem::size_of::<Option<i32>>() is not the same as std::mem::size_of::<Option<i64>>(). A null pointer to both i32 and i64 is size_of::<usize>().

1 Like

In this reply I interpret your "allocation" definition as "on the heap".[1] And use terms like "allocates" analogously.


Option never allocates on its own. Option<T> is a tagged union that could be a T or could be nothing (None). It is always an inline value; there is no indirection where the Option<T> is on the stack but the T is on the heap, for example. If the Option<T> is on the stack and is the Some variant, the T is also on the stack. If the Option<T> is on the heap and is the Some variant, the T is also on the heap.

Option::<T>::Some and Option::<T>::None have the same size. Indeed, they are the same type (Option<T>) and the type has a consistent size known at compile time. Sometimes this is the size of T plus some more for the tag; sometimes this is the size of T exactly (e.g. because T has a restricted bit pattern and the tag can be represented by some bit pattern which is not a valid T).

So if an Option<T> is on the heap, for example if you have a Box<Option<T>>, some allocation will be present even if the value is None.[2]

If an Option<T> is on the stack and the T potentially allocates -- say T is a String -- than a None implies no allocation as there is no potentially allocating value present. If the Option is Some... there may or may not be an allocation! String::new() doesn't allocate for example.

In this example, the String itself[3] would be on the stack, because again, Option<T> stores T inline when present.

Even if the the Option<T> is Some and an allocation has occurred because T allocates, it's not the Option that allocated heap memory. The Option::<T>::Some owns the T, but that T is, again, inline. If T happened to allocate memory, dropping the Option will drop the T and the T will[4] deallocate the memory. But the Option isn't aware of the allocation and deallocation (or lack of thereof). Indeed, Option is defined in core, where there is no concept of a heap at all. All it knows is that it sometimes own a T.


If we're talking about null pointers, there is a specific FFI targeted carve-out where, for example, an Option::<&SizedType>::None corresponds to the null pointer. But since you mentioned allocations, I don't think this is what you were really talking about. Rust references can point anywhere, not just the heap, so the presence of a reference doesn't imply a heap allocation. This is a pointer concern, not an allocation concern, per se.

Similar to the String example, given a Option::<&SizedType>::Some value, the Option doesn't "care" or "know" if there's an allocation behind the reference being stored inline or not.


I don't know what language you're coming from. In some languages, any non-null value may imply an allocation because every "present value" is a heap allocation (or at least potentially auto-boxed).

But not everything in Rust is boxed / an allocation. So while Option::None may be analogous to the null of those languages in the sense of "there's no value present", there's not necessarily the same 1-to-1 correspondence with allocations.


  1. in contrast with LLVM terminology, say ↩︎

  2. Assuming the size of Option<T> is not zero, which will pretty much always be the case; AFAIK the only exception is when T is uninhabited. Ignore this footnote if it makes no sense to you. ↩︎

  3. which consists of a pointer, length, and capacity ↩︎

  4. assuming a typical, well-behaved, non-leaking type ↩︎

14 Likes

It sounds like you might be thinking of languages which "box" their objects implicitly, i.e. heap-allocate them.

Rust doesn't box anything unless if you explicitly ask for it. To give an example, Option<i32> lives entirely on the stack, while Option<Box<i32>> might allocate heap memory.

5 Likes

And, for completeness, Box<Option<u32>> will always allocate heap memory, even if you put None into it.

3 Likes

This isn't true, either. Rust doesn't implicitly heap-allocate, but the type system doesn't know (or care) about dynamic allocation. Furthermore, types can be composed in arbitrary ways, so Option<T> doesn't mean "always a top-level Option<T>, never wrapped in anything else".

Consequently, if you only know you have an Option<T>, then that's the only thing you know about it. It can be on the stack, on the heap, in static memory, or wherever. The type itself doesn't know, care about, or express that information.

What matters is the place and construction of bindings. Rust puts function-local variables "on the stack" (although this is an implementation detail and is usually heavily affected by optimizations). This has nothing to do with whether they themselves perform additional heap-allocation. If you do

let b = Box::new(0);

then the pointer (ie., the box) itself still isn't dynamically allocated, only the content it points to.

So talking about such memory regions and allocation strategies merely using types is not correct, you also have to consider the bindings at all times.

6 Likes

It was an oversimplification on my part, though your statement here would suggest that something like let _: Option<i32> is not guaranteed to stack-allocate? Let's assume for simplicity that it doesn't cross an await point, is there an ABI or optimization pass which can put a local binding on the heap?

1 Like

That one can be optimized out completely.

But another possibility for a variable is putting it into a register.

(And you said " Option<i32> lives entirely on the stack", not something about variables specifically.)

4 Likes

No. It means precisely what I wrote. I.e., that Option<i32>, the type itself, doesn't imply anything about the heap or the stack.

Your let _: Option<i32> is not just a type. It contains a binding too.

By the way, I don't think Rust makes any particular guarantees about the place of any variables, even locals. Although it's generally expected that locals won't heap allocate, they are definitely not guaranteed to stack-allocate either, because e.g. they can be promoted to statics, allocated to registers, and/or optimized away completely.

2 Likes

If the local variable is inside an async block or function, it may well end up on the heap if it's used on both sides of an await, as the future object it's a part of can be stored there. A similar thing can also happen with move closures.

2 Likes

Oh yes, and then there's that. So it's even fewer guarantees, actually.


Although, to be honest, I'd still not count e.g. the closure scenario as "a local variable caused a heap allocation". Closures don't heap-allocate by themselves, either, but they are often manually boxed for example, when used in a type-erased manner. So, for example, considering the following snippet:

let x: i32 = 0;
let func = move || println!("{x}");
let erased: Box<dyn FnMut()> = Box::new(func);

although the local variable x captured by the function will eventually end up on the heap, I wouldn't say that "the variable x caused a heap allocation". The variable x was moved to the heap by the act of moving its capturing closure to the heap, which was done very deliberately by an actor completely independent of x. This is nothing that x itself could have caused or prevented.

So I think it's more correct to say that "local variables can be on the heap" or "local variables can end up on the heap when moved transitively", rather than "a local variable in itself caused a heap allocation".

2 Likes