Call Stack

Overview

The call stack is what a program uses to keep track of function calls. The call stack is made up of stack frames—one for each function call.

For instance, say we called a function that rolled two dice and printed the sum.

def roll_die(): return random.randint(1, 6) def roll_two_and_sum(): total = 0 total += roll_die() total += roll_die() print total roll_two_and_sum()

First, our program calls roll_two_and_sum. It goes on the call stack:

roll_two_and_sum

That function calls roll_die, which gets pushed on to the top of the call stack:

roll_die
roll_two_and_sum

Inside of roll_die, we call random.randint. Here's what our call stack looks like then:

random.randint
roll_die
roll_two_and_sum

When random.randint finishes, we return back to roll_die by removing ("popping") random.randint's stack frame.

roll_die
roll_two_and_sum

Same thing when roll_die returns:

roll_two_and_sum

We're not done yet! roll_two_and_sum calls roll_die again:

roll_die
roll_two_and_sum

Which calls random.randint again:

random.randint
roll_die
roll_two_and_sum

random.randint returns, then roll_die returns, putting us back in roll_two_and_sum:

roll_two_and_sum

Which calls print:

print
roll_two_and_sum

What's stored in a stack frame?

What actually goes in a function's stack frame?

A stack frame usually stores:

  • Local variables
  • Arguments passed into the function
  • Information about the caller's stack frame
  • The return address—what the program should do after the function returns (i.e.: where it should "return to"). This is usually somewhere in the middle of the caller's code.

Some of the specifics vary between processor architectures. For instance, AMD64 (64-bit x86) processors pass some arguments in registers and some on the call stack. And, ARM processors (common in phones) store the return address in a special register instead of putting it on the call stack.

The Space Cost of Stack Frames

Each function call creates its own stack frame, taking up space on the call stack. That's important because it can impact the space complexity of an algorithm. Especially when we use recursion.

For example, if we wanted to multiply all the numbers between 1 and n, we could use this recursive approach:

def product_1_to_n(n): return 1 if n <= 1 else n * product_1_to_n(n - 1)

What would the call stack look like when n = 10?

First, product_1_to_n gets called with n = 10:

    product_1_to_n    n = 10

This calls product_1_to_n with n = 9.

    product_1_to_n    n = 9
    product_1_to_n    n = 10

Which calls product_1_to_n with n = 8.

    product_1_to_n    n = 8
    product_1_to_n    n = 9
    product_1_to_n    n = 10

And so on until we get to n = 1.

    product_1_to_n    n = 1
    product_1_to_n    n = 2
    product_1_to_n    n = 3
    product_1_to_n    n = 4
    product_1_to_n    n = 5
    product_1_to_n    n = 6
    product_1_to_n    n = 7
    product_1_to_n    n = 8
    product_1_to_n    n = 9
    product_1_to_n    n = 10

Look at the size of all those stack frames! The entire call stack takes up space. That's right—we have an space cost even though our function itself doesn't create any data structures!

What if we'd used an iterative approach instead of a recursive one?

def product_1_to_n(n): # We assume n >= 1 result = 1 for num in range(1, n + 1): result *= num return result

This version takes a constant amount of space. At the beginning of the loop, the call stack looks like this:

    product_1_to_n    n = 10, result = 1, num = 1

As we iterate through the loop, the local variables change, but we stay in the same stack frame because we don't call any other functions.

    product_1_to_n    n = 10, result = 2, num = 2

    product_1_to_n    n = 10, result = 6, num = 3

    product_1_to_n    n = 10, result = 24, num = 4

In general, even though the compiler or interpreter will take care of managing the call stack for you, it's important to consider the depth of the call stack when analyzing the space complexity of an algorithm.

Be especially careful with recursive functions! They can end up building huge call stacks.

What happens if we run out of space? It's a stack overflow! In Python 2.7, you'll get a RecursionError.

If the very last thing a function does is call another function, then its stack frame might not be needed any more. The function could free up its stack frame before doing its final call, saving space.

This is called tail call optimization (TCO). If a recursive function is optimized with TCO, then it may not end up with a big call stack.

In general, most languages don't provide TCO. Scheme is one of the few languages that guarantee tail call optimization. Some Ruby, C, and Javascript implementations may do it. Python and Java decidedly don't.

What's next?

If you're ready to start applying these concepts to some problems, check out our mock coding interview questions.

They mimic a real interview by offering hints when you're stuck or you're missing an optimization.

Try some questions now

. . .