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?
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>()
.
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.
in contrast with LLVM terminology, say ↩︎
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. ↩︎
which consists of a pointer, length, and capacity ↩︎
assuming a typical, well-behaved, non-leaking type ↩︎
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.
And, for completeness, Box<Option<u32>>
will always allocate heap memory, even if you put None
into it.
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.
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?
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.)
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 static
s, allocated to registers, and/or optimized away completely.
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.
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".
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.