g ++中尾部递归的问题

时间:2010-12-21 08:25:16

标签: c++ recursion functional-programming g++ tail-recursion

我正在用C ++搞乱尾递归函数,而且我对g ++编译器遇到了一些麻烦。

numbers[]大小超过几百个整数时,以下代码会导致堆栈溢出。检查g ++为以下内容生成的汇编代码,发现twoSum_Helper正在对自身执行递归call指令。

问题是以下哪一项导致了这个问题?

  • 以下我忽略了一个错误,它阻止了尾部递归。
  • 使用g ++时出错。
  • g ++编译器中检测尾递归函数的缺陷。

我正在使用g + 4.5.0通过MinGW在Windows Vista x64上编译g++ -O3 -Wall -fno-stack-protector test.c

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);
}

8 个答案:

答案 0 :(得分:4)

C或C ++中的尾调用优化非常有限,而且几乎是一个失败的原因。原因是通常没有安全的方法来从一个将指针或引用传递给任何局部变量的函数进行尾调用(作为相关调用的参数,或者实际上是同一函数中的任何其他调用) - 当然,这一切都发生在C / C ++的各个地方,几乎不可能没有。

您看到的问题可能是相关的:GCC可能通过实际传递在调用者堆栈上分配的隐藏变量的地址来编译返回结构,被调用者将其复制到其中 - 这使其落入上述场景中。 / p>

答案 1 :(得分:2)

尝试使用-O2代替-O3。

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

嗯,无论如何它都不适用于O2。唯一似乎有用的是将result对象返回给作为参数的引用。

但实际上,删除Tail调用并使用循环更容易。 TCO用于优化内联时或执行激进展开时发现的尾调用,但是在处理大值时不应尝试使用递归。

答案 2 :(得分:1)

即使在这个简单的函数上,我也无法使用g ++ 4.4.0(在mingw下)执行尾递归:

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

我尝试过-O3-O2-fno-stack-protector,C和C ++变体。没有尾递归。

答案 3 :(得分:0)

我会看两件事。

  1. if 语句中的返回调用将在堆栈框架中为 else 创建一个分支目标,用于当前需要的函数运行在调用后解决(这意味着任何TCO尝试都不能覆盖正在执行的堆栈帧,从而否定TCO)

  2. numbers [] 数组参数是一个可变长度数据结构,它也可以防止TCO,因为在TCO中,相同的堆栈帧以某种方式使用。如果调用是自引用(与您的一样),那么它将使用新调用的值/引用覆盖堆栈定义的变量(或本地定义的变量)。如果尾调用是另一个函数,则它将用新函数覆盖整个堆栈帧(在T =可以在A => B =&gt; C中完成的情况下,TCO可以使其看起来像A =&gt;执行期间内存中的C)。我会尝试指针。

  3. 自从我用C ++构建任何东西已经有几个月了,所以我没有运行任何测试,但我认为其中一个/两个都阻止了优化。

答案 4 :(得分:0)

尝试将代码更改为:

// 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);
}

编辑:根据提问者的评论删除不必要的参数

// 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);
}

答案 5 :(得分:0)

支持尾部调用优化(TCO)在C / C ++中受到限制。

因此,如果代码依赖于TCO来避免堆栈溢出,那么用循环重写它可能会更好。否则需要进行一些自动测试以确保代码已经过优化。

通常可以通过以下方式抑制TCO:

  • 将指针传递给递归函数堆栈上的对象到外部函数(如果C ++也通过引用传递这样的对象);
  • 具有非平凡析构函数的本地对象,即使尾递归有效(析构函数在尾 Statement statement = connection.createStatement(); String queryParam = "Computers" String sqlQuery = "select employee_id,first_name,last_name from dfs.'employee.json' where department LIKE'" +queryParam +"'"+"and conditions<...> "; ResultSet rs = statement.executeQuery(sqlQuery); while(rs.next) { do as you need } 语句之前调用),例如Why isn't g++ tail call optimizing while gcc is?

这里通过按值返回结构来混淆TCO。 如果所有递归调用的结果都被写入在其他函数return中分配的相同内存地址(类似于答案https://stackoverflow.com/a/30090390/4023446Tail-recursion not happening

,则可以修复它。
twoSum

对于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); } 的所有递归调用,res_指针的值是常量。 在程序集输出(-S标志)中可以看出,即使有两个递归出口点,twoSum_Helper尾递归也被优化为循环。

编译选项:g ++ -O2 -S(g ++版本4.7.2)。

答案 6 :(得分:-2)

我听到其他人抱怨,尾递归只是用gcc优化而不是g ++。 你能尝试使用gcc吗?

答案 7 :(得分:-3)

由于twoSum_Helper的代码正在调用自身,因此程序集显示正好发生的事情并不令人意外。这就是递归的全部要点:-)所以这与g ++没有任何关系。

每次递归都会创建一个新的堆栈帧,默认情况下堆栈空间是有限的。您可以增加堆栈大小(不知道如何在Windows上执行此操作,在UNIX上使用ulimit命令),但这只会延迟崩溃。

真正的解决方案是摆脱递归。例如,请参阅this questionthis question