【问题标题】:Why does this constexpr code cause GCC to eat all my RAM?为什么这个 constexpr 代码会导致 GCC 吃掉我所有的 RAM?
【发布时间】:2012-11-04 06:51:24
【问题描述】:

以下程序将调用 fun 2 ^ (MAXD + 1) 次。不过,最大递归深度不应该超过 MAXD(如果我的想法是正确的)。因此编译可能需要一些时间,但它不应该吃掉我的 RAM。

#include<iostream>

const int MAXD = 20;

constexpr int fun(int x, int depth=0){
  return depth == MAXD ? x : fun(fun(x + 1, depth + 1) + 1, depth + 1);
}

int main(){
  constexpr int i = fun(1);
  std::cout << i << std::endl;
}

问题在于,吃掉我的内存正是它的作用。当我将 MAXD 提高到 30 时,我的笔记本电脑在 GCC 4.7.2 快速分配 3 GB 左右后开始交换。我还没有尝试使用 clang 3.1,因为我现在无法访问它。

我唯一的猜测是,这与 GCC 试图过于聪明并记住函数调用有关,就像它对模板所做的那样。如果是这样,他们没有限制他们做多少记忆,比如 MRU 缓存表的大小或其他东西,这似乎并不奇怪?我还没有找到禁用它的开关。

我为什么要这样做? 我正在考虑制作一个高级编译时库的想法,比如基因编程之类的。由于编译器没有编译时尾调用优化,我担心任何循环都需要递归并且(即使我调高最大递归深度参数,这看起来有点难看)会快速分配我所有的 RAM 并填充它带有毫无意义的堆栈帧。因此,我想出了上述解决方案,用于在没有深堆栈的情况下获得任意多个函数调用。这样的功能可以用于折叠/循环或蹦床。

编辑: 现在我已经在 clang 3.1 中尝试过了,它根本不会泄漏内存,无论我让它工作多长时间(即我做 MAXD 多高)。 CPU 使用率几乎为 100%,内存使用率几乎为 0%,正如预期的那样。也许这只是 GCC 中的一个错误。

【问题讨论】:

  • 我已经确认堆栈永远不会超过 MAXD(如预期的那样),通过运行函数运行时并观察到虽然我可以让它运行很长时间,但它根本不使用 RAM。
  • 您是否应该按照gcc.gnu.org/bugs 中的建议报告此问题?
  • @osgx 这并不是一个真正的错误,不是吗?根据标准,我想他们可以对我的 RAM 做他们喜欢做的事。另外,我希望有人知道他们在做什么(你知道你是谁;)告诉我原因是什么。
  • 好的,现在我已经在 clang 3.1 上测试过了。 Clang 产生“正确”的行为并且不消耗 RAM。似乎这是 GCC 的错误或功能错误。
  • 好的,我已将其报告为错误:gcc.gnu.org/bugzilla/show_bug.cgi?id=55442

标签: c++ gcc c++11 metaprogramming constexpr


【解决方案1】:

这可能不是关于 constexpr 的最终文档,但它是从 gcc constexpr wiki. 链接到的主要文档

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2235.pdf

...它说...

我们(仍然)禁止常量表达式中所有形式的递归。 这不是绝对必要的,因为实施限制 常量表达式求值中的递归深度将使我们免于 编译器永远递归的可能性。然而,直到我们 看到一个令人信服的递归用例,我们不建议允许它。

所以,我希望您遇到语言边界和 gcc 选择实现 constexpr 的方式(可能尝试内联生成整个函数,然后评估/执行它)

【讨论】:

  • 那个文档太旧了,我想。该标准不禁止递归,但建议默认限制为 512 次递归,gcc 和 clang 都尊重这一点,但允许用户覆盖。您可能是对的,问题不是记忆(尽管我知道 gcc 对 constexprs 这样做),而是它内联扩展了代码。
  • @Gurgeh - 是的 - 这似乎是 ISO 标准的一个常见问题,除了实际标准本身之外,网络上散布着所有可能的文档 :-) 是对“递归次数”的推荐限制" 还是递归深度?在你的情况下,深度是 20,但正如你所说,“递归次数”可以被认为是 2^MAXD-1...
  • 绝对递归深度。一个比 512 更深的简单递归(用内部 fun(...) 代替 x 并增加 MAXD)会使编译器对你发誓。较浅的不会。
【解决方案2】:

您的答案在您的评论中“通过运行函数运行时并观察到虽然我可以让它运行很长时间”,这是由您对 fun(x + 1, depth + 1) 的内部最递归调用引起的.

当您通过删除 constexpr 将其更改为运行时函数而不是编译时函数并观察到它运行了很长时间时,这表明它的递归非常深入。

当编译器执行函数时,它必须深度递归,但不使用堆栈进行递归,因为它实际上并没有生成和执行机器代码。

【讨论】:

  • 当然会。它返回就好了。你试过代码了吗?
  • 深度递归并不是一个东西可以长时间运行的唯一方法。它也可以广泛地分支,这就是我的代码所做的。
  • 您的代码没有广泛分支。它只有一个分支,并且在每次递归时都在分支的同一侧,直到它停止递归。
  • 你写的所有东西都完全错了。 1) gcc 和 clang 都有一个 512 编译时间的递归限制,除非你另外设置。如果您打破此限制,编译将失败并显示错误消息。因此我知道代码不会在 512 以下递归。2)深度参数应该通过检查递归不低于 MAXD 的代码来明确。
  • 3) 它确实“广泛”地递归。想象没有内在乐趣的乐趣。这将导致 MAXD 调用,因为它只是循环到底部。一种链表。增加的内在乐趣意味着在到达 MAXD 的每一步中,都会产生一个子树,当它前进到 MAXD 时,它又会产生子树。它将产生一种二叉树。输出证实了这一点。 4) 编译后的代码也将使用堆栈进行递归,除非使用了优化开关,它执行 TCO。 5) clang 没有表现出这种行为,这表明它是 GCC 的一个记忆问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-05-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-19
  • 2011-03-16
  • 2012-08-27
相关资源
最近更新 更多