RustArena

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

Common misconceptions

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

Practice it