CD Notes
Guide to stack-based memory allocation for automatic variables and function calls
Introduction to Stack Memory
Imagine a stack of plates in a cafeteria. You can only place a new plate on top, and you can only remove the plate that is currently on top. This Last-In-First-Out (LIFO) principle is exactly how stack memory works in your programs. Every time a function is called, a new "plate" (called a stack frame) is placed on top containing all the function's local variables. When the function returns, that plate is removed, and all its variables vanish instantly.
Stack allocation is the workhorse of memory management in compiled programs. It is incredibly fast — allocating memory is as simple as moving a single pointer — and completely automatic, requiring no explicit cleanup from the programmer. The compiler generates all the necessary instructions to manage the stack, making it invisible to the programmer in most situations.
Why the Stack is Fast
To allocate memory on the stack, the CPU simply decrements the stack pointer (SP) by the required number of bytes. To deallocate, it increments SP back. There is no searching for free blocks, no metadata to update, no fragmentation to worry about. The entire operation takes a single instruction:
Compare this to heap allocation, which involves calling malloc(), searching free lists, splitting blocks, and updating metadata — potentially hundreds of instructions for a single allocation. The stack achieves O(1) allocation in practice, not just in theory.
How the Compiler Uses the Stack
When the compiler processes a function, it calculates the total size needed for all local variables and allocates them as a single block by adjusting the stack pointer. Consider this C function:
The compiler generates a prologue (entry code) and epilogue (exit code) for every function:
Notice how deallocation is instantaneous — just restore the old stack pointer. All local variables are destroyed simultaneously regardless of how many there were.
Stack Frame Layout
Each function call creates a stack frame (also called activation record) with a well-defined structure:
The frame pointer (RBP) provides a stable reference for accessing local variables. Even as the stack pointer changes during the function (due to pushing temporary values), all local variables remain at fixed offsets from RBP.
Scope and Variable Lifetime
Stack allocation perfectly aligns with lexical scoping rules. Variables are alive exactly as long as their enclosing scope exists:
void example() {
int x = 10; // x alive from here
{
int y = 20; // y alive from here
int z = x + y; // z alive from here
} // y and z dead here (scope ends)
printf("%d\n", x); // x still alive
// printf("%d\n", y); // ERROR: y no longer exists
} // x dead hereIn nested scopes, the compiler can reuse the same stack space for variables in non-overlapping scopes:
The Call Stack: Nested Function Calls
When functions call other functions, stack frames pile up. Consider this recursive example:
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main() {
int result = factorial(4);
}The call stack grows with each recursive call:
| │ factorial(1) | n=1 │ ← Top (most recent call) |
| │ factorial(2) | n=2 │ |
| │ factorial(3) | n=3 │ |
| │ factorial(4) | n=4 │ |
Each recursive call gets its own independent copy of n because each has its own stack frame. This is why recursion works — there is no confusion between the different instances of the same variable.
Stack Overflow
The stack has a fixed maximum size (typically 1-8 MB on modern systems). If your program exceeds this limit, you get a stack overflow — one of the most common runtime errors. The usual causes are:
Deep recursion without a base case, or with input too large:
int infinite_recursion(int n) {
return infinite_recursion(n + 1); // Never stops → stack overflow
}Large local arrays consuming excessive stack space:
Stack vs Heap: When to Use Each
| Criterion | Stack | Heap |
|---|---|---|
| Size known at compile time? | Yes → Stack | No → Heap |
| Needs to outlive function? | No → Stack | Yes → Heap |
| Very large allocation? | Risky → Heap | Safe → Heap |
| Performance critical? | Stack (faster) | Heap (slower) |
| Shared between threads? | Not easily | Yes (with sync) |
The Compiler's Optimization Opportunities
Modern compilers perform several stack-related optimizations. Stack slot reuse allows variables with non-overlapping lifetimes to share the same memory location. Register promotion moves frequently accessed stack variables into CPU registers entirely, eliminating memory access. Tail call optimization reuses the current stack frame for a recursive call in tail position, converting recursion into iteration and preventing stack overflow.
Key Takeaways
Stack allocation is the foundation of function-based programming. Its LIFO discipline perfectly matches the nested nature of function calls — last called, first returned. The compiler handles all stack management automatically, generating prologue and epilogue code for each function. The key limitations are fixed size (determined at compile time) and limited lifetime (tied to function scope). Understanding stack allocation helps you write efficient code, avoid stack overflows, and appreciate why certain programming patterns (like returning pointers to local variables) lead to bugs.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Stack Allocation: Automatic Memory Management.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Compiler Design topic.
Search Terms
compiler-design, compiler design, compiler, design, runtime, environment, stack, allocation
Related Compiler Design Topics