← Back to blog Khalil Drissi

Stack vs heap: how memory actually works

Listen to article
0:00

People talk about “the stack” and “the heap” like they are physical objects you can point at. They are not. They are two regions of the same virtual address space, managed in completely different ways. I spent years writing C before I really internalized the difference, and once I did, a lot of confusing bugs suddenly made sense.

The stack is a region, not a data structure

When your program starts, the operating system hands each thread a chunk of contiguous memory called the stack. It grows in one direction, usually downward toward lower addresses on x86 and ARM. Every time you call a function, the CPU pushes a stack frame: the return address, saved registers, and room for local variables. When the function returns, that frame is gone instantly. No bookkeeping, no search, just a single register adjustment.

That is why stack allocation is fast. There is a register, the stack pointer, and “allocating” 64 bytes means subtracting 64 from it. Freeing means adding it back. The cost is effectively zero.

The catch is lifetime. A stack variable lives exactly as long as the function call that created it. Return a pointer to a local and you are pointing at memory that the next function call will scribble over.

// This is a bug. The buffer dies when the function returns.
char *make_greeting(void) {
    char buffer[32];
    snprintf(buffer, sizeof buffer, "hello");
    return buffer;   // dangling pointer
}

The heap is for things that outlive a frame

The heap is the rest of your usable address space, and it is managed by an allocator (malloc and friends) rather than by the CPU. When you ask for memory you get a block that stays valid until you explicitly free it. That flexibility is the whole point, and it is also where the work hides. The allocator has to track which blocks are free, find one big enough, and hand it back. I wrote a whole post on writing a simple memory allocator because that machinery is worth understanding directly.

char *make_greeting(void) {
    char *buffer = malloc(32);   // lives on the heap
    if (!buffer) return NULL;
    snprintf(buffer, 32, "hello");
    return buffer;   // valid, but the caller now owns it
}

The tradeoffs you actually feel

Why this connects to performance

The locality point is the one that bites in real systems. A pointer to the heap is a value, and following that pointer is a value too. Where it physically lands decides whether your CPU stalls. I dig into that in data-oriented design and CPU caches, but the short version is that scattering your data across heap allocations can be slower than the algorithm would suggest, purely because of cache misses.

What the addresses look like

If you print the address of a local variable and the address of a malloc result in the same program, the difference is stark. Stack addresses tend to be high and close together, because everything in the current call chain sits in one tight region. Heap addresses sit lower and spread out as the heap grows upward to meet the stack growing downward. They are marching toward each other through the same address space, and in the old days a program that used too much of both would have them collide. Virtual memory and guard pages make that collision a clean crash today instead of silent corruption.

One more thing worth knowing: allocation on the stack is not just fast, it is also automatically aligned and laid out by the compiler, which knows every local’s size up front. The allocator has to compute alignment and padding at runtime. That extra work is part of why the heap costs more per allocation, on top of the bookkeeping.

A mental model that holds up

Here is how I think about it now. The stack is a scratchpad the CPU manages for you, perfect for short-lived, known-size values. The heap is a warehouse you manage yourself, for anything whose size or lifetime you cannot pin down at compile time. Most bugs in C come from confusing the two: returning stack pointers, freeing heap memory twice, or forgetting to free it at all.

The reason languages like Rust feel safe is that they encode these rules into the type system so the compiler catches the mistakes. If you want to see how that works without a garbage collector, read memory safety with Rust ownership and borrowing. But you cannot really appreciate what Rust is protecting you from until you have felt the sharp edges of the stack and the heap yourself.

Comments
Leave a comment