11

I'm was messing around with tail-recursive functions in C++, and I've run into a bit of a snag with the g++ compiler.

The following code results in a stack overflow when numbers[] is over a couple hundred integers in size. Examining the assembly code generated by g++ for the following reveals that twoSum_Helper is executing a recursive call instruction to itself.

The question is which of the following is causing this?

  • A mistake in the following that I am overlooking which prevents tail-recursion.
  • A mistake with my usage of g++.
  • A flaw in the detection of tail-recursive functions within the g++ compiler.

I am compiling with g++ -O3 -Wall -fno-stack-protector test.c on Windows Vista x64 via MinGW with g++ 4.5.0.

struct result
{
    int i;
    int j;
    bool found;
};

struct result gen_Result(int i, int j, bool found)
{
    struct result r;
    r.i = i;
    r.j = j;
    r.found = found;
    return r;
}

// Return 2 indexes from numbers that sum up to target.
struct result twoSum_Helper(int numbers[], int size, int target, int i, int j)
{
    if (numbers[i] + numbers[j] == target)
        return gen_Result(i, j, true);
    if (i >= (size - 1))
        return gen_Result(i, j, false);
    if (j >= size)
        return twoSum_Helper(numbers, size, target, i + 1, i + 2);
    else
        return twoSum_Helper(numbers, size, target, i, j + 1);
}
6
  • 1
    Did you already try to do the conditional increments separately and do the recursive call only once with the incremented parameters? It is less nice than your example, but it might shed some light on your problem. Commented Dec 21, 2010 at 8:37
  • @stefaanv Yes, to no avail. It appears that the call is occuring on the else statement, but no amount of tweaking will cause it to use a jmp instead of a call. Commented Dec 21, 2010 at 9:31
  • 1
    Does it work if you use a single statement ala return twoSum_Helper(numbers, size, target, i + j_ge_size, j_ge_size ? i + 2 : j + 1) where j_ge_size is bool j >= size? (suit yourself re implicit conversion from bool). Commented Dec 21, 2010 at 9:43
  • @Tony Assembly still has a call to itself for what equates to the else clause above. Commented Dec 21, 2010 at 9:52
  • 1
    Tom makes an interesting observation in stackoverflow.com/questions/34125 - his tail recursion needed the function to be static...? Commented Dec 21, 2010 at 10:09

8 Answers 8

4

Tail call optimization in C or C++ is extremely limited, and pretty much a lost cause. The reason is that there generally is no safe way to tail-call from a function that passes a pointer or reference to any local variable (as an argument to the call in question, or in fact any other call in the same function) -- which of course is happening all over the place in C/C++ land, and is almost impossible to live without.

The problem you are seeing is probably related: GCC likely compiles returning a struct by actually passing the address of a hidden variable allocated on the caller's stack into which the callee copies it -- which makes it fall into the above scenario.

Sign up to request clarification or add additional context in comments.

Comments

1

Try compilling with -O2 instead of -O3.

How do I check if gcc is performing tail-recursion optimization?

well, it doesn't work with O2 anyway. The only thing that seems to work is returning the result object into a reference that is given as a parameter.

but really, it's much easier to just remove the Tail call and use a loop instead. TCO is here to optimize tail call that are found when inlining or when performing agressive unrolling, but you shouldn't attempt to use recursion when handling large values anyway.

2 Comments

If you're curious, the flag for optimizing tail recursion is -foptimize-sibling-calls, and is included in -O2, -O3, and -Os.
Well, the answer i'm linking to presents a case where O2 works, but not O3.
1

I can't get g++ 4.4.0 (under mingw) to perform tail recursion, even on this simple function:

static void f (int x)
  {
  if (x == 0) return ;
  printf ("%p\n", &x) ; // or cout in C++, if you prefer
  f (x - 1) ;
  }

I've tried -O3, -O2, -fno-stack-protector, C and C++ variants. No tail recursion.

Comments

0

I would look at 2 things.

  1. The return call in the if statement is going to have a branch target for the else in the stack frame for the current run of the function that needs to be resolved post call (which would mean any TCO attempt would not be able overwrite the executing stack frame thus negating the TCO)

  2. The numbers[] array argument is a variable length data structure which could also prevent TCO because in TCO the same stack frame is used in one way or another. If the call is self referencing (like yours) then it will overwrite the stack defined variables (or locally defined) with the values/references of the new call. If the tail call is to another function then it will overwrite the entire stack frame with the new function (in a case where TCO can be done in A => B => C, TCO could make this look like A => C in memory during execution). I would try a pointer.

It has been a couple months since I have built anything in C++ so I didn't run any tests, but I think one/both of those are preventing the optimization.

3 Comments

There is no “numbers[] array argument”, that’s misleading (and it certainly isn’t of variable length). There is only a pointer argument, the stack frame is always used identically, and that’s almost certainly not the cause here. And if-else isn’t the cause either. Every recursion needs a base case, and hence a conditional statement.
Has been a while since C++, I get that it is a pointer, thnx. I do think the conditional is the source though, recursion does need a base case, but Tail Call optimization means that there is no instruction left on the stack except return after the recursive call has been made, in order for the compiler to optimize, the recursive call needs to be the last statement in the frame, the 2nd IF still has a branch target for the ELSE after it in the execution stack. It is the logical last statement but not the physical (when the assembly is in memory).
And I did mean the 3rd IF (the one that decides the next self referencing recursive call)
0

Try changing your code to:

// Return 2 indexes from numbers that sum up to target.
struct result twoSum_Helper(int numbers[], int size, int target, int i, int j)
{
    if (numbers[i] + numbers[j] == target)
        return gen_Result(i, j, true);
    if (i >= (size - 1))
        return gen_Result(i, j, false);

    if(j >= size)
        i++; //call by value, changing i here does not matter
    return twoSum_Helper(numbers, size, target, i, i + 1);
}

edit: removed unnecessary parameter as per comment from asker

// Return 2 indexes from numbers that sum up to target.
struct result twoSum_Helper(int numbers[], int size, int target, int i)
{
    if (numbers[i] + numbers[i+1] == target || i >= (size - 1))
        return gen_Result(i, i+1, true);

    if(i+1 >= size)
        i++; //call by value, changing i here does not matter
    return twoSum_Helper(numbers, size, target, i);
}

4 Comments

This will probably work, but unfortunately it defeats the general idea of what I was testing.
Also, j is not necessarily always going to be i+1 on every recursive call.
@Swiss I just rewrote the bottom lines of zour code and kept the function signature, but since zou asked for it I changed the singature and shortened it a bit.
@Swiss Also what would be the problem if this would work, this would mean that the problem is likely that you have two points from which you recurse. As the example shows this can easily be rewritten (like recursion could also be removed by using loops, I think there is a theorem for that, since I could not google it quick enough there is a section in wikipedia wich hints to this)
0

Support of Tail Call Optimization (TCO) is limited in C/C++.

So, if the code relies on TCO to avoid stack overflow it may be better to rewrite it with a loop. Otherwise some auto test is needed to be sure that the code is optimized.

Typically TCO may be suppressed by:

  • passing pointers to objects on stack of recursive function to external functions (in case of C++ also passing such object by reference);
  • local object with non-trivial destructor even if the tail recursion is valid (the destructor is called before the tail return statement), for example Why isn't g++ tail call optimizing while gcc is?

Here TCO is confused by returning structure by value. It can be fixed if the result of all recursive calls will be written to the same memory address allocated in other function twoSum (similarly to the answer https://stackoverflow.com/a/30090390/4023446 to Tail-recursion not happening)

struct result
{
    int i;
    int j;
    bool found;
};

struct result gen_Result(int i, int j, bool found)
{
    struct result r;
    r.i = i;
    r.j = j;
    r.found = found;
    return r;
}

struct result* twoSum_Helper(int numbers[], int size, int target,
    int i, int j, struct result* res_)
{
    if (i >= (size - 1)) {
        *res_ = gen_Result(i, j, false);
        return res_;
    }
    if (numbers[i] + numbers[j] == target) {
        *res_ = gen_Result(i, j, true);
        return res_;
    }
    if (j >= size)
        return twoSum_Helper(numbers, size, target, i + 1, i + 2, res_);
    else
        return twoSum_Helper(numbers, size, target, i, j + 1, res_);
}

// Return 2 indexes from numbers that sum up to target.
struct result twoSum(int numbers[], int size, int target)
{
    struct result r;
    return *twoSum_Helper(numbers, size, target, 0, 1, &r);
}

The value of res_ pointer is constant for all recursive calls of twoSum_Helper. It can be seen in the assembly output (the -S flag) that the twoSum_Helper tail recursion is optimized as a loop even with two recursive exit points.

Compile options: g++ -O2 -S (g++ version 4.7.2).

Comments

-2

I have heard others complain, that tail recursion is only optimized with gcc and not g++. Could you try using gcc.

Comments

-3

Since the code of twoSum_Helper is calling itself it shouldn't come as a surprise that the assembly shows exactly that happening. That's the whole point of a recursion :-) So this hasn't got anything to do with g++.

Every recursion creates a new stack frame, and stack space is limited by default. You can increase the stack size (don't know how to do that on Windows, on UNIX the ulimit command is used), but that only defers the crash.

The real solution is to get rid of the recursion. See for example this question and this question.

6 Comments

Tail recursive functions are unique because it is theoretically possible to optimize their recursion from a call instruction to a jmp instruction. This changes the stack requirement of the function from O(n) to O(1). In practice it's not so simple, because it relies on the compiler to make the distinction and utilize the optimization.
This doesn't answer the question: tail recursion should elide the call and prevent the stack from overflowing, but the optimisation is not being applied, and the OP wants to know why.
Sorry, I've missed that there's an optimization which is not applied since this wasn't mentioned in the question. Still, it's a bit dangerous to rely on a specific compiler optimization, no ?
@DarkDust I'm starting to feel that way about tail-recursion. It's nice in theory, but can go terribly wrong with no real warnings.
@Swiss: that's a C++ problem (or in general a language-specific problem). There are languages which guarantee that tail recursion is done. For people experimenting with their own compilers, the easiest way to start is by adding an explicit tailcall %1 instruction.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.