Stack vs heap in Rust
Where Rust stores your data: the stack for fixed-size values, the heap for String, Vec and Box, how ownership frees heap memory, and the costs.
A running program keeps data in two regions of memory, and knowing which one a value uses explains most of Rust's ownership rules.
The stack
Each function call gets a stack frame holding its local variables. When the function returns, the frame is discarded in one step. Allocation is just moving a pointer, so it's extremely fast, and the memory is right next to other recently used data (good for the CPU cache).
The rule: every value on the stack must have a size known at compile time. i32 (4 bytes), f64 (8), [u8; 100] (100), a struct of those, a tuple of those.
The heap
Data whose size is only known at run time, or that grows, goes on the heap: a large pool where the program allocates blocks of any size while it runs. Allocating means asking the allocator for space (slower than the stack), and every block must later be freed exactly once.
In Rust, heap data always has an owner on the stack:
| Type | On the stack | On the heap |
|---|---|---|
i32, f64, bool, char |
the value | nothing |
[i32; 4], tuples, plain structs |
all of it | nothing |
String |
pointer, length, capacity (24 bytes) | the text |
Vec<T> |
pointer, length, capacity (24 bytes) | the elements |
Box<T> |
a pointer (8 bytes) | the T |
&str, &[T] |
pointer and length (16 bytes) | nothing of its own: it borrows |
(Sizes are for a 64-bit machine.) When the owner goes out of scope, Rust's generated drop code frees the heap block. That's ownership: the compiler knows statically where each heap block is freed, so there's no garbage collector and no manual free.
Why moves exist
Copying a String handle would leave two handles pointing at the same heap text, and both would free it. So assigning a String moves it (the old variable becomes unusable) instead of copying. Stack-only types like i32 have nothing to double-free, so they are simply copied.
Box: putting a value on the heap on purpose
Box::new(value) moves a value to the heap and keeps a pointer to it. You need it for recursive types, whose size would otherwise be infinite:
enum List {
Node(i32, List),
Empty,
}
fn main() {}
error[E0072]: recursive type `List` has infinite size
Node(i32, Box<List>) fixes it: a Box is always one pointer wide. Box is also used for trait objects (Box<dyn Trait>) and to move very large values off the stack.
Performance notes
- Allocation isn't free. In hot loops, reuse buffers (
buf.clear()keeps the capacity) and pre-size collections withVec::with_capacity(n)orString::with_capacity(n). - The stack is small: the main thread typically gets 8 MB on Linux (1 MB on Windows), and spawned threads get 2 MB by default. Huge local arrays or very deep recursion end with
thread 'main' has overflowed its stack. Use aVecfor big buffers. - Moving a
StringorVeccopies only the 24-byte handle, never the heap data.
Common misconceptions
- "Structs live on the heap." No: a struct lives wherever you put it. A local struct is on the stack; its
Stringfields point to the heap. - "String literals are on the heap."
"hello"is stored in the program binary.String::from("hello")copies it into a new heap block.
Example
use std::mem::size_of;
fn main() {
println!("i64: {} bytes", size_of::<i64>());
println!("[u8; 100]: {} bytes", size_of::<[u8; 100]>());
println!("String: {} bytes", size_of::<String>());
println!("Vec<u64>: {} bytes", size_of::<Vec<u64>>());
println!("Box<[u8; 100]>: {} bytes", size_of::<Box<[u8; 100]>>());
println!("&str: {} bytes", size_of::<&str>());
let small = String::from("hi");
let large = "x".repeat(10_000);
println!("handles: {} and {} bytes", size_of_val(&small), size_of_val(&large));
println!("heap text: {} and {} bytes", small.len(), large.len());
let mut readings = Vec::with_capacity(4);
for r in [3, 1, 4, 1] {
readings.push(r);
}
println!("{} readings, capacity {}", readings.len(), readings.capacity());
let boxed: Box<i32> = Box::new(41);
println!("boxed value + 1 = {}", *boxed + 1);
}
Output:
i64: 8 bytes
[u8; 100]: 100 bytes
String: 24 bytes
Vec<u64>: 24 bytes
Box<[u8; 100]>: 8 bytes
&str: 16 bytes
handles: 24 and 24 bytes
heap text: 2 and 10000 bytes
4 readings, capacity 4
boxed value + 1 = 42