g++ 中的尾递归问题

发布于 2024-10-08 06:36:11 字数 1083 浏览 8 评论 0原文

我正在搞乱 C++ 中的尾递归函数,并且在使用 g++ 编译器时遇到了一些障碍。

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

问题是以下哪一项导致了这种情况?

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

我正在 Windows Vista x64 上通过 MinGW 使用 g++ 4.5.0 使用 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);
}

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

你与昨日 2024-10-15 06:36:11

C 或 C++ 中的尾部调用优化极其有限,而且几乎是失败的原因。原因是,通常没有安全的方法从传递指针或对任何局部变量的引用的函数进行尾部调用(作为相关调用的参数,或者实际上是同一函数中的任何其他调用)——当然,这种情况在 C/C++ 领域随处可见,而且几乎是离不开的。

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

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.

我不是你的备胎 2024-10-15 06:36:11

尝试使用 -O2 而不是 -O3 进行编译。

如何检查 gcc 是否正在执行尾递归优化?

好吧,无论如何,它不适用于 O2。似乎唯一有效的方法是将结果对象返回到作为参数给出的引用中。

但实际上,删除 Tail 调用并使用循环会更容易。 TCO 在这里是为了优化内联或执行积极展开时发现的尾部调用,但无论如何,您不应该在处理大值时尝试使用递归。

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.

撑一把青伞 2024-10-15 06:36:11

即使在这个简单的函数上,我也无法让 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++ 变体。没有尾递归。

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.

茶花眉 2024-10-15 06:36:11

我会看两件事。

  1. if 语句中的 return 调用将为当前运行需要解析的函数的堆栈帧中的 else 提供一个分支目标后调用(这意味着任何 TCO 尝试都无法覆盖正在执行的堆栈帧,从而否定 TCO)

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

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

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.

jJeQQOZ5 2024-10-15 06:36:11

尝试将您的代码更改为:

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

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);
}
作妖 2024-10-15 06:36:11

C/C++ 中对尾部调用优化 (TCO) 的支持有限。

因此,如果代码依赖 TCO 来避免堆栈溢出,那么最好用循环重写它。否则需要一些自动测试来确保代码得到优化。

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

  • 将递归函数堆栈上的对象的指针传递给外部函数(在 C++ 的情况下也通过引用传递此类对象);
  • 具有非平凡析构函数的本地对象,即使尾递归有效(析构函数在尾 return 语句之前调用),例如 为什么 g++ 尾部调用不优化,而 gcc 却优化?

这里 TCO 通过按值返回结构而感到困惑。
如果所有递归调用的结果都写入其他函数 twoSum 中分配的相同内存地址,则可以修复它(类似于答案 https://stackoverflow.com/a/30090390/4023446尾递归未发生

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

对于 twoSum_Helper 的所有递归调用,res_ 指针的值都是常量。
从汇编输出(-S 标志)中可以看出,即使有两个递归退出点,twoSum_Helper 尾递归也被优化为循环。

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

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).

意犹 2024-10-15 06:36:11

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

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

蹲墙角沉默 2024-10-15 06:36:11

由于 twoSum_Helper 的代码正在调用自身,因此程序集准确显示发生的情况也就不足为奇了。这就是递归的全部意义:-) 所以这与 g++ 没有任何关系。

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

真正的解决方案是摆脱递归。例如,请参阅此问题这个问题

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文