constexpr函数求值可以做尾递归优化吗?

Can constexpr function evaluation do tail recursion optimization

本文关键字:尾递归 优化 函数 constexpr      更新时间:2023-10-16

我想知道对于长循环,我们是否可以利用 C++11 中 constexpr 的尾递归?

根据[implimits]的规则,一个实现被允许对constexpr计算设置递归深度限制。具有完整constexpr实现的两个编译器(gcc 和 clang(都应用了这样的限制,使用标准建议的默认值 512 递归调用。对于这两个编译器,以及遵循标准建议的任何其他实现,尾递归优化基本上是无法检测到的(除非编译器在达到其递归限制之前崩溃(。

相反,实现可以选择仅计算无法在其递归深度限制中应用尾递归优化的调用,或者不提供此类限制。但是,这样的实现可能会对其用户造成伤害,因为它可能会崩溃(由于堆栈溢出(或无法在深度或无限递归的constexpr评估中终止。

关于达到递归深度限制时会发生什么,Pubby的例子提出了一个有趣的观点。 [expr.const]p2指定

对 constexpr

函数或 constexpr 构造函数的调用将超出实现定义的递归限制(参见附件 B(;

不是一个常量表达式。因此,如果在需要常量表达式的上下文中达到递归限制,则程序格式不正确。如果在不需要常量表达式的上下文中调用 constexpr 函数,则实现通常不需要在转换时尝试计算它,但如果它选择这样做,并且达到递归限制,则需要改为在运行时执行调用。在完整、可编译的测试程序上:

constexpr unsigned long long f(unsigned long long n, unsigned long long s=0) {
  return n ? f(n-1,s+n) : s;
}
constexpr unsigned long long k = f(0xffffffff);

海湾合作委员会 说:

depthlimit.cpp:4:46:   in constexpr expansion of ‘f(4294967295ull, 0ull)’
depthlimit.cpp:2:23:   in constexpr expansion of ‘f((n + -1ull), (s + n))’
depthlimit.cpp:2:23:   in constexpr expansion of ‘f((n + -1ull), (s + n))’
[... over 500 more copies of the previous message cut ...]
depthlimit.cpp:2:23:   in constexpr expansion of ‘f((n + -1ull), (s + n))’
depthlimit.cpp:4:46: error: constexpr evaluation depth exceeds maximum of 512 (use -fconstexpr-depth= to increase the maximum)

叮当 说:

depthlimit.cpp:4:30: error: constexpr variable 'k' must be initialized by a constant expression
constexpr unsigned long long k = f(0xffffffff);
                             ^   ~~~~~~~~~~~~~
depthlimit.cpp:2:14: note: constexpr evaluation exceeded maximum depth of 512 calls
  return n ? f(n-1,s+n) : s;
             ^
depthlimit.cpp:2:14: note: in call to 'f(4294966784, 2194728157440)'
depthlimit.cpp:2:14: note: in call to 'f(4294966785, 2190433190655)'
depthlimit.cpp:2:14: note: in call to 'f(4294966786, 2186138223869)'
depthlimit.cpp:2:14: note: in call to 'f(4294966787, 2181843257082)'
depthlimit.cpp:2:14: note: in call to 'f(4294966788, 2177548290294)'
depthlimit.cpp:2:14: note: (skipping 502 calls in backtrace; use -fconstexpr-backtrace-limit=0 to see all)
depthlimit.cpp:2:14: note: in call to 'f(4294967291, 17179869174)'
depthlimit.cpp:2:14: note: in call to 'f(4294967292, 12884901882)'
depthlimit.cpp:2:14: note: in call to 'f(4294967293, 8589934589)'
depthlimit.cpp:2:14: note: in call to 'f(4294967294, 4294967295)'
depthlimit.cpp:4:34: note: in call to 'f(4294967295, 0)'
constexpr unsigned long long k = f(0xffffffff);
                                 ^

如果我们修改代码,以便不需要在翻译时进行评估:

constexpr unsigned long long f(unsigned long long n, unsigned long long s=0) {
  return n ? f(n-1,s+n) : s;
}
int main(int, char *[]) {
  return f(0xffffffff);
}

然后两个编译器都接受它,并生成在运行时计算结果的代码。使用 -O0 构建时,此代码由于堆栈溢出而失败。当使用 -O2 构建时,编译器的优化器将代码转换为使用尾递归,并且代码函数正确(但请注意,此尾递归与constexpr计算无关(。

我不明白为什么这是不可能的,但它是实现细节的质量。

例如,传统上对

模板使用记忆,这样编译器就不会再窒息:

template <size_t N>
struct Fib { static size_t const value = Fib <N-1>::value + Fib<N-2>::value; };
template <>
struct Fib<1> { static size_t const value = 1; }
template <>
struct Fib<0> { static size_t const value = 0; }

而是记住已经计算的值,以将其评估的复杂性降低到 O(N(。

尾递归

(和伪尾递归(是优化,并且像大多数优化一样不受标准约束,因此没有理由不可能。但是,很难预测特定编译器是否使用它。

该标准在5.19 [expr.const]中说:

2/条件表达式是核心常量表达式,除非它涉及以下作为潜在评估的子表达式之一 (3.2( [...]:

    对 constexpr
  • 函数或 constexpr 构造函数的调用将超出实现定义的递归限制(参见附件 B(;

并阅读附件B:

2/这些限制可能会限制包括下述数量或其他数量。建议将每个数量后面的括号数字作为该数量的最小值。但是,这些数量只是指导原则,并不能确定合规性。

  • 递归 constexpr 函数调用 [512]。

尾递归不胸针。

我不确定我是否理解你在问什么。 如果涉及是否编译器会将尾递归转换为循环,它未指定,函数是否为constexpr。 如果是递归函数可以是constexpr,那我不认为尾巴递归是相关的。 如果我正确阅读了标准:

constexpr unsigned ack( unsigned m, unsigned n )
{
    return m == 0
        ? n + 1
        : n == 0
        ? ack( m - 1, 1 )
        : ack( m - 1, ack( m, n - 1 ) );
}

是一个有效的 constexpr(尽管我希望编译器会抱怨除了最小的nm之外,所有资源都缺乏资源,至少如果该函数用于需要常量表达式的上下文中(。

我已经看到GCC执行了这种优化。下面是一个示例:

constexpr unsigned long long fun1(unsigned long long n, unsigned long long sum = 0) {
  return (n != 0) ? fun1(n-1,sum+n) : sum;
}
fun1(0xFFFFFFFF);

在 -O2 上工作,否则崩溃。

令人惊讶的是,它也在优化这一点:

constexpr unsigned long long fun2(unsigned long long n) {
  return (n != 0) ? n + fun2(n-1) : 0;
}

我已经检查了非 conspexpr 表单的反汇编,我可以确认它正在被优化为循环。

但不是这个:

constexpr unsigned long long fun3(unsigned long long n) {
  return (n != 0) ? n + fun3(n-1) + fun3(n-1) : 0;
}

因此,总而言之,GCC 将优化到一个循环中,就像它对非 consexpr 函数所做的那样。至少使用 -O2 及以上。

">

Tail call"可能一开始就用词不当。 constexpr函数更接近数学函数。对于数学函数,以下两个函数是相同的:

constexpr unsigned long long fun1(unsigned long long n) {
  if (n == 0) return 0 ;
  return n + fun1(n-1);
}
constexpr unsigned long long fun2(unsigned long long n) {
  if (n != 0) return n + fun2(n-1);
  return  0;
}

然而,从过程编程的角度来看,它们绝对不是。只有第一个似乎适合尾部调用优化。