Mojo Stack vs Heap
Every value your program creates lives somewhere in memory. That somewhere is either the stack or the heap — two completely different regions with different rules, speeds, and lifetimes. Understanding the difference explains why Mojo programs are fast, why ownership matters, and how to avoid memory bugs.
The Two Memory Regions
RAM (your computer's working memory) ┌──────────────────────────────────────────────────┐ │ │ │ Stack Heap │ │ ───────────────────── ────────────────── │ │ Grows downward ↓ Grows anywhere │ │ Auto-managed Manually managed │ │ Very fast Slightly slower │ │ Fixed-size values Variable-size values │ │ Short lifetime Long lifetime │ │ │ └──────────────────────────────────────────────────┘
The Stack: A Plate Pile
Think of the stack like a stack of cafeteria plates.
You can only add or remove from the TOP.
fn main(): → push frame for main()
var x = 10 → push x=10 onto stack
fn do_work(): → push frame for do_work()
var y = 20 → push y=20 onto stack
← do_work ends → pop y, pop do_work frame
← main ends → pop x, pop main frame
Stack (top is most recent):
┌──────────┐ ← top (newest)
│ y = 20 │
├──────────┤
│ do_work │
├──────────┤
│ x = 10 │
├──────────┤
│ main │
└──────────┘ ← bottom (oldest)
Stack Characteristics
Allocated: when a variable is declared (var x = 10)
Freed: when the variable goes out of scope (automatic)
Speed: single CPU instruction to allocate or free
Size limit: typically 1-8 MB (system-dependent)
Lives: only within the scope it was declared in
fn main():
var a = 42 ← a allocated on stack
{
var b = 99 ← b allocated on stack
} ← b freed here (out of scope)
print(a) ← a still on stack
← a freed here (end of main)
What Lives on the Stack
In Mojo, these types live on the stack by default:
Int, Float64, Bool → fixed-size scalar types
SIMD[DType.float32, 4] → fixed-size SIMD vectors
Small structs → if all fields are stack types
Function call frames → return address, local variables
Stack allocation example:
var temp: Float64 = 3.14 ← 8 bytes reserved, instant
var v = SIMD[DType.int32, 4](1,2,3,4) ← 16 bytes, instant
The Heap: A Rented Storage Unit
The heap is a large pool of memory that programs share. You rent space (alloc) and must return it when done (free). Storage unit analogy: ┌─────────────────────────────────────────────────────┐ │ HEAP (storage facility) │ │ │ │ [ Unit A: List data ] [ Unit B: String chars ] │ │ [ Unit C: your array ] [ Unit D: free space ] │ │ │ │ You get a key (pointer) when you rent. │ │ Return the key (free) when done. │ └─────────────────────────────────────────────────────┘
Heap Characteristics
Allocated: explicitly with UnsafePointer.alloc() or by List/String Freed: explicitly with .free() or automatically via __del__ Speed: slower than stack (must find free block, update bookkeeping) Size limit: practically limited only by available RAM Lives: as long as you keep the pointer alive var buf = UnsafePointer[Float32].alloc(1000) ← 4000 bytes reserved on heap (1000 × 4 bytes) ... buf.free() ← returned to heap
What Lives on the Heap
In Mojo, these types store data on the heap:
String → character buffer on heap, pointer on stack
List[T] → element buffer on heap, metadata on stack
Dict[K,V] → hash table on heap
UnsafePointer.alloc(n) → explicit heap allocation
Stack part ←────────────────────────→ Heap part
┌──────────────────────┐ ┌──────────────────┐
│ String: │ │ "Hello, Mojo!" │
│ ptr ────────────────┼──────────────→│ (the characters) │
│ len = 13 │ └──────────────────┘
│ cap = 16 │
└──────────────────────┘
The stack holds metadata (pointer + length).
The actual character data lives on the heap.
Side-by-Side Comparison
Property │ Stack │ Heap ──────────────────┼────────────────────────┼──────────────────────── Allocation speed │ O(1) — single instr. │ O(1) amortised, slower Deallocation │ Automatic at scope end │ Manual or via __del__ Size flexibility │ Fixed at compile time │ Can grow/shrink at runtime Max size │ ~1–8 MB typically │ Limited by RAM only Cache performance │ Excellent (contiguous) │ Variable (fragmentation) Who manages it │ Compiler/OS │ You (or smart wrappers) Memory leak risk │ None — automatic │ Yes, if free() forgotten
Mojo's Ownership System Bridges Both
struct DynamicBuffer:
var data: UnsafePointer[Float32] ← pointer lives on STACK
var size: Int ← size lives on STACK
fn __init__(inout self, n: Int):
self.size = n
self.data = UnsafePointer[Float32].alloc(n) ← data on HEAP
fn __del__(owned self):
self.data.free() ← Mojo calls this automatically when
DynamicBuffer goes out of scope
fn main():
var buf = DynamicBuffer(1024) ← stack var, heap data
# ... use buf ...
← buf goes out of scope:
__del__ called automatically
heap memory freed
no leak possible
Why Mojo Is Fast: Stack-First Design
Python object model (heap-heavy): x = 42 → allocates a Python int object on heap → stores type info, reference count, and value → heap lookup required to read x Mojo scalar model (stack-first): var x: Int = 42 → 8 bytes directly in a CPU register or stack slot → no heap access, no indirection, no reference counting → read/write in a single CPU cycle For numerical workloads, this difference means Mojo loops run 10–100× faster than equivalent Python.
Practical Rules
Use the stack (Mojo default) when: ✓ Size is known at compile time ✓ Data lives only within one function or scope ✓ Maximum performance required Use the heap when: ✓ Size is unknown until runtime (user input, file content) ✓ Data must outlive the function that created it ✓ Very large data (> stack size limit) Wrap heap allocations in a struct with __del__ so cleanup is automatic — never leave raw UnsafePointer without a wrapper.
Key Takeaways
The stack stores fixed-size values and is managed automatically — values are freed when their scope ends. The heap stores dynamically sized data and requires explicit allocation and deallocation. Stack allocation is a single CPU instruction; heap allocation involves bookkeeping and is slower. Mojo scalar types (Int, Float64, SIMD) live on the stack by default, making numerical code extremely fast. String, List, and UnsafePointer.alloc() use the heap. Wrap heap allocations in structs with __del__ so Mojo's ownership system frees memory automatically when the struct goes out of scope — preventing leaks without a garbage collector.
