How exactly does tail recursion work?

CAlgorithmRecursionTail Recursion

C Problem Overview


I almost understand how tail recursion works and the difference between it and a normal recursion. I only don't understand why it doesn't require stack to remember its return address.

// tail recursion
int fac_times (int n, int acc) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}
     
int factorial (int n) {
    return fac_times (n, 1);
}

// normal recursion
int factorial (int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

There is nothing to do after calling a function itself in a tail recursion function but it doesn't make sense for me.

C Solutions


Solution 1 - C

The compiler is simply able to transform this

int fac_times (int n, int acc) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

into something like this:

int fac_times (int n, int acc) {
label:
    if (n == 0) return acc;
    acc *= n--;
    goto label;
}

Solution 2 - C

You ask why "it doesn't require stack to remember its return address".

I would like to turn this around. It does use the stack to remember the return address. The trick is that the function in which the tail recursion occurs has its own return address on the stack, and when it jumps to the called function, it will treat this as it's own return address.

Concretely, without tail call optimization:

f: ...
   CALL g
   RET
g:
   ...
   RET

In this case, when g is called, the stack will look like:

   SP ->  Return address of "g"
          Return address of "f"

On the other hand, with tail call optimization:

f: ...
   JUMP g
g:
   ...
   RET

In this case, when g is called, the stack will look like:

   SP ->  Return address of "f"

Clearly, when g returns, it will return to the location where f was called from.

EDIT: The example above use the case where one function calls another function. The mechanism is identical when the function calls itself.

Solution 3 - C

Tail recursion can usually be transformed into a loop by the compiler, especially when accumulators are used.

// tail recursion
int fac_times (int n, int acc = 1) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

would compile to something like

// accumulator
int fac_times (int n) {
    int acc = 1;
    while (n > 0) {
        acc *= n;
        n -= 1;
    }
    return acc;
}

Solution 4 - C

There are two elements that must be present in a recursive function:
  1. The recursive call
  2. A place to keep count of the return values.
A "regular" recursive function keeps (2) in the stack frame.

The return values in regular recursive function are composed of two types of values:

  • Other return values
  • Result of the owns function computation

Let's look at your example:

int factorial (int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

The frame f(5) "stores" the result of it's own computation (5) and the value of f(4), for example. If i call factorial(5), just before the stack calls begin to collapse, i have:

 [Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]

Notice that each stack stores, besides the values i mentioned, the whole scope of the function. So, the memory usage for a recursive function f is O(x), where x is the number of recursive calls i have to made. So, if i needb 1kb of RAM to calculate factorial(1) or factorial(2), i need ~100k to calculate factorial(100), and so on.

A Tail Recursive function put (2) in it's arguments.

In a Tail Recursion, i pass the result of the partial calculations in each recursive frame to the next one using parameters. Let's see our factorial example, Tail Recursive:

int factorial (int n) {
    int helper(int num, int accumulated)
        {
            if num == 0 return accumulated
            else return helper(num - 1, accumulated*num)
        }
    return helper(n, 1)    
}

Let's look at it's frames in factorial(4):

[Stack f(4, 5): Stack f(3, 20): [Stack f(2,60): [Stack f(1, 120): 120]]]]

See the differences? In "regular" recursive calls the return functions recursively compose the final value. In Tail Recursion they only reference the base case (last one evaluated). We call accumulator the argument that keeps track of the older values.

Recursion Templates

The regular recursive function go as follows:

type regular(n)
    base_case
    computation
    return (result of computation) combined with (regular(n towards base case))

To transform it in a Tail recursion we:

  • Introduce a helper function that carries the accumulator
  • run the helper function inside the main function, with the accumulator set to the base case.

Look:

type tail(n):
    type helper(n, accumulator):
        if n == base case
            return accumulator
        computation
        accumulator = computation combined with accumulator
        return helper(n towards base case, accumulator)
    helper(n, base case)

See the difference?

Tail Call optimization

Since no state is being stored on the Non-Border-Cases of the Tail Call stacks, they aren't so important. Some languages/interpreters then substitute the old stack with the new one. So, with no stack frames constraining the number of calls, the Tail Calls behave just like a for-loop in these cases.

It's up to your compiler to optimize it, or no.

Solution 5 - C

Here is a simple example that shows how recursive functions work:

long f (long n)
{

    if (n == 0) // have we reached the bottom of the ocean ?
        return 0;

    // code executed in the descendence

    return f(n-1) + 1; // recurrence

    // code executed in the ascendence

}

Tail recursion is a simple recursive function, where recurrence is done at the end of the function, thus no code is done in ascendence, which helps most compilers of high-level programming languages to do what is known as Tail Recursion Optimization, also has a more complex optimization known as the Tail recursion modulo

Solution 6 - C

The recursive function is a function which calls by itself

It allows programmers to write efficient programs using a minimal amount of code.

The downside is that they can cause infinite loops and other unexpected results if not written properly.

I will explain both Simple Recursive function and Tail Recursive function

In order to write a Simple recursive function

  1. The first point to consider is when should you decide on coming out of the loop which is the if loop
  2. The second is what process to do if we are our own function

From the given example:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

From the above example

if(n <=1)
     return 1;

Is the deciding factor when to exit the loop

else 
     return n * fact(n-1);

Is the actual processing to be done

Let me the break the task one by one for easy understanding.

Let us see what happens internally if I run fact(4)

  1. Substituting n=4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

If loop fails so it goes to else loop so it returns 4 * fact(3)

  1. In stack memory, we have 4 * fact(3)

Substituting n=3

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

If loop fails so it goes to else loop

so it returns 3 * fact(2)

Remember we called ```4 * fact(3)``

The output for fact(3) = 3 * fact(2)

So far the stack has 4 * fact(3) = 4 * 3 * fact(2)

  1. In stack memory, we have 4 * 3 * fact(2)

    Substituting n=2

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

If loop fails so it goes to else loop

so it returns 2 * fact(1)

Remember we called 4 * 3 * fact(2)

The output for fact(2) = 2 * fact(1)

So far the stack has 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. In stack memory, we have 4 * 3 * 2 * fact(1)

    Substituting n=1

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If loop is true

so it returns 1

Remember we called 4 * 3 * 2 * fact(1)

The output for fact(1) = 1

So far the stack has 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

Finally, the result of fact(4) = 4 * 3 * 2 * 1 = 24

enter image description here

The Tail Recursion would be

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}

  1. Substituting n=4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

If loop fails so it goes to else loop so it returns fact(3, 4)

  1. In stack memory, we have fact(3, 4)

    Substituting n=3

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

If loop fails so it goes to else loop

so it returns fact(2, 12)

  1. In stack memory, we have fact(2, 12)

    Substituting n=2

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

If loop fails so it goes to else loop

so it returns fact(1, 24)

  1. In stack memory, we have fact(1, 24)

    Substituting n=1

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If loop is true

so it returns running_total

The output for running_total = 24

Finally, the result of fact(4,1) = 24

enter image description here

Solution 7 - C

My answer is more of a guess, because recursion is something relating to internal implementation.

In tail recursion, the recursive function is called at the end of the same function. Probably compiler can optimize in below way:

  1. Let the ongoing function wind up (i.e. used stack is recalled)
  2. Store the variables which are going to be used as arguments to the function in a temporary storage
  3. After this, call the function again with the temporarily stored argument

As you can see, we are winding up the original function before the next iteration of the same function, so we are not actually "using" the stack.

But I believe if there are destructors to be called inside the function then this optimization may not apply.

Solution 8 - C

Compiler is enough intelligent to understand tail recursion.In case, while returning back from a recursive call,there is no pending operation and recursive call is the last statement, fall under the category of tail recursion. Compiler basically performs tail recursion optimization, removing stack implementation.Consider below code.

void tail(int i) {
    if(i<=0) return;
    else {
     system.out.print(i+"");
     tail(i-1);
    }
   }

After performing optimization , the above code is converted into below one.

void tail(int i) {
    blockToJump:{
    if(i<=0) return;
    else {
     system.out.print(i+"");
     i=i-1;
     continue blockToJump;  //jump to the bolckToJump
    }
    }
   }

This is how compiler does Tail Recursion Optimization.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlan CoromanoView Question on Stackoverflow
Solution 1 - CAlexey FrunzeView Answer on Stackoverflow
Solution 2 - CLindydancerView Answer on Stackoverflow
Solution 3 - CmepcotterellView Answer on Stackoverflow
Solution 4 - CLucas RibeiroView Answer on Stackoverflow
Solution 5 - CKhaled.KView Answer on Stackoverflow
Solution 6 - CNursnaazView Answer on Stackoverflow
Solution 7 - CiammilindView Answer on Stackoverflow
Solution 8 - CVarunnuevothoughtsView Answer on Stackoverflow