在c++中做尾递归

Doing tail recursion in C++

本文关键字:尾递归 c++      更新时间:2023-10-16

如果我做一个尾部调用递归(与for (;;)...break循环相反),我的函数可以写得更简单。但是,我担心如果编译器不能对它进行优化,我会遇到性能问题,特别是因为它将由最终用户编译。

  1. 是否有办法告诉编译器"确保你优化了这个尾部调用,否则给我一个错误"(例如Scala支持这个)

  2. 如果编译器不能优化它,性能限制是什么?在不破坏堆栈的情况下,我可以运行多少次尾部调用?


更新:

编译器是gcc和MSVC。

一般来说,我预计会有十几个尾巴电话。但在极端情况下,可能有数千个。平台是典型的低端笔记本电脑(如酷睿i3或i5)。

不,没有办法告诉编译器需要尾部递归。一些编译器(据我所知没有)可能支持特定于实现的注释,但这需要用户使用特定的编译器。在某些模式下,其他一些编译器故意不支持尾部调用,因为它们可以通过不支持尾部调用来提供更好的调试体验。用户可能正在使用这样的编译器。

允许的递归深度高度依赖于程序、函数和实现,无法给出合理的数字。给定一个特定的平台,您可能可以确定默认的堆栈大小,调查该平台上一个特定编译器的帧大小,并进行简单的除法,以大致估计可以有多少个嵌套调用。

我建议以一种方式重写它,使读者清楚发生了什么,但不依赖于编译器优化尾部调用。虽然很讨厌,但是goto语句在这方面非常有用。

取一个简单的尾递归位计数函数:

int bitcount(unsigned int n, int acc = 0) {
  if (n == 0)
    return acc;
  return bitcount(n >> 1, acc + (n & 1));
}

可以简单地重写为

int bitcount(unsigned int n, int acc = 0) {
tail_recurse:
  if (n == 0)
    return acc;
  // tail return bitcount(n >> 1, acc + (n & 1));
  acc += n & 1;
  n = n >> 1;
  goto tail_recurse;
}
当然,这是一个简单的版本,它被简单地重写以完全避免递归,甚至可能不应该手动实现,但是我在这里使用的特定转换可以应用于任何函数,其中可能存在尾递归并且需要尾递归。注释应该确保读者仍然可以很容易地发现发生了什么。

对于GCC,您可以使用backtrace()函数添加运行时检查:

#include <cassert>
#include <iostream>
#include <execinfo.h>
size_t start;
size_t stack_frames()
{
  void *array[16];
  size_t size = backtrace(array, 16);
  // std::cout << "Obtained " << size << " stack frames.n";
  return size;
}
bool missing_tail()
{
  return stack_frames() > start + 2;
}
int bitcount(unsigned int n, int acc = 0)
{
  assert(!missing_tail());
  if (n == 0)
    return acc;
  return bitcount(n >> 1, acc + (n & 1));
}
int main()
{
  start = stack_frames();
  std::cout << bitcount(10) << 'n';
  return 0;
}

当使用低于-O2的优化级别(无尾递归优化)编译时,您将获得断言失败