G++ 尾递归优化失败

Posted

技术标签:

【中文标题】G++ 尾递归优化失败【英文标题】:G++ tail recursion optimization failing 【发布时间】:2020-08-01 05:37:17 【问题描述】:

我提出这个问题的依据是我对上一个问题 here 的评论。一位用户回答说我可以拥有无​​限的尾递归堆栈。然而,这不是我在实践中发现的。为了说明我的观点,看看我的代码:

#include <iostream>
#include <string>

void tail_print(string& in, size_t& index) //prints in backwards

  if (index == 0)
    
      cout << '$' << endl;
      return;
    

  cout << in[index];
  index--;
  tail_print(in, index);


int main()

  string a("abc$");
  size_t pos = a.length()-1;
  tail_print(a, pos);
  return 0;

假设输入字符串 in 包含介于以下范围之间的字符:1

代码编译为:g++ -pipe -std=c++14 -O2 $file -lm -o exe

这会引发信号 11 (SIGSEG)。我无法准确判断哪个输入失败,但我可以肯定地说这个信号的原因来自这个子程序(如果我用 for 循环向后打印字符,效果很好)。请注意,这是一个更大计划的一部分,因此可能会出现不可预见的并发症(机会很小)。无论哪种方式,如果尾递归优化导致 *** 在微不足道的 O(百万) 深度,我不得不对它提出一些疑问。

我正在使用以下 g++ 版本:

~$ g++ --version
g++ (Ubuntu 5.5.0-12ubuntu1~16.04) 5.5.0 20171010

【问题讨论】:

为什么将零索引视为退出条件?是否将字符串中的字符从 1 迭代到字符串长度? 请提供minimal reproducible example @AlanBirtles 添加了 main() 并包含。请注意,我没有添加上面提到的失败输入。它可能取决于机器。我无法访问引发段错误的原始机器。 GCC 10 可以优化尾递归:godbolt.org/z/q6M487 早期版本出于某种原因不能。鉴于尾递归代码可以轻松地替换为循环,为什么还要冒险? 看起来它只是你的编译器的版本。 GCC 9.1 does not do tail recursion。 GCC 10.1 does。这就是你的 C++。尾递归没有任何保证。仅仅因为某些东西看起来是尾递归并不意味着编译器会发出尾递归代码。 【参考方案1】:

如果您依赖尾递归,那么编译器是否会选择优化您的代码,您将受到编译器的支配。调试构建不会被优化,所以总是会失败。

在您的情况下,通过std::cout 打印单个字符似乎是您的问题的原因。 libstdc++ 似乎通过调用来实现打印单个字符:

std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)

由于某种原因,这似乎会导致 GCC 10 之前的尾递归优化出现问题。所有版本的 Clang 也无法对此进行优化。

std::cout.put(in[index]) 替换cout &lt;&lt; in[index] 似乎允许所有版本的GCC(至少低至4.1.2)和Clang 来优化尾递归:https://godbolt.org/z/Th1bT8

有趣的是,直接调用 std::__ostream_insert 也可以(但不要这样做,因为你依赖于内部 libstdc++ 实现细节):https://godbolt.org/z/9M5xd4

我认为通过libstdc++ 中的各个级别的函数调用,您最终会得到(由于函数char 参数被值采用):

char c = in[index];
std::__ostream_insert<char, std::char_traits<char> >(std::cout, &c, 1);

创建一个指向局部变量的指针似乎是防止尾递归的原因:https://godbolt.org/z/KM4jGY,大概这是因为编译器不知道被调用的函数将对该指针做什么,所以它不能保证使用循环将具有相同的行为。

由于所有尾递归都应该可以用循环轻松替换,因此最好不要依赖编译器的变幻莫测来为您完成它,即使在未优化的构建中也可以这样做:

void tail_print(const std::string& in, size_t index) //prints in backwards

    for (size_t i = index; i > 0; i--)
    
        std::cout << in[i];
    
    std::cout << "$\n";

【讨论】:

以上是关于G++ 尾递归优化失败的主要内容,如果未能解决你的问题,请参考以下文章

尾递归优化

如何看待以及理解Python的这种尾递归优化

Python开启尾递归优化!

Oz 中的尾递归优化

kotlin学习笔记之尾递归优化(tailrec)

kotlin学习笔记之尾递归优化(tailrec)