constexpr函数评估可以进行尾递归优化

时间:2012-02-13 09:56:18

标签: c++ c++11 language-lawyer constexpr

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

5 个答案:

答案 0 :(得分:20)

根据[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)

和clang说:

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求值无关。)

答案 1 :(得分:4)

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

传统上使用memoization作为模板,因此编译器不再窒息:

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]。
  •   

尾部递归并未打破。

答案 2 :(得分:2)

我不确定我明白你在问什么。如果它涉及是否 编译器会将尾递归转换为循环,它是未指定的, 该函数是否为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之外,所有人都缺乏资源,至少是 该函数用于需要常量表达式的上下文中。

答案 3 :(得分:-1)

我见过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及以上。

答案 4 :(得分:-2)

“尾巴召唤”可能是一个误称的开头。 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;
}
但从程序编程的角度来看,他们肯定不是。只有第一个似乎适合尾部调用优化。