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.

Leave a Comment

Your email address will not be published. Required fields are marked *