Optimization 递归开销——有多严重?

Optimization 递归开销——有多严重?,optimization,programming-languages,recursion,tail-recursion,interpreted-language,Optimization,Programming Languages,Recursion,Tail Recursion,Interpreted Language,可能重复: 大约15年前,我第一次接受了认真的C语言编程培训。我的雇主想要高度优化的代码来完成计算困难的任务。我记得有人不止一次建议我将递归重写为循环,即使可读性很高,以避免“递归开销”。我当时的理解是,递归开销是将数据推到堆栈上,然后将其弹出所需的额外工作 现在我用C、Python、Perl编写代码,有时用Java编写代码,有时还想知道递归。重写它们还有什么好处吗?如果它们是尾部递归呢?现代的编译器是否让所有这些问题变得毫无意义?这些问题与解释语言无关吗?我认为您提到的任何语言都不需要平台/

可能重复:

大约15年前,我第一次接受了认真的C语言编程培训。我的雇主想要高度优化的代码来完成计算困难的任务。我记得有人不止一次建议我将递归重写为循环,即使可读性很高,以避免“递归开销”。我当时的理解是,递归开销是将数据推到堆栈上,然后将其弹出所需的额外工作


现在我用C、Python、Perl编写代码,有时用Java编写代码,有时还想知道递归。重写它们还有什么好处吗?如果它们是尾部递归呢?现代的编译器是否让所有这些问题变得毫无意义?这些问题与解释语言无关吗?

我认为您提到的任何语言都不需要平台/编译器实现。您可以找到确实需要这种优化的语言—大多数函数式语言都有这种要求

然而,你需要考虑的另一件事是,计算机的数量级已经比15年前快了很多,所以现在比以前更稀少了,你需要担心微优化。一个15年前可能需要在汇编程序中仔细手工优化才能获得良好性能的程序,在现代计算机上可能运行得极快,即使是用Java等高级语言编写的。这并不是说性能不再是一个问题——但您应该集中精力选择正确的算法和编写可读代码。只有在测量了性能之后,才能进行微优化,并且您可以看到有问题的代码是瓶颈


不过,您确实需要担心的一件事是堆栈溢出。如果有发生这种情况的任何风险,可能值得用迭代的方式重写递归函数。

这是严重的。我编写的大多数语言都有函数调用的实际成本(它们的编译器通常也可以进行尾部递归,所以有时这不是问题)

这一成本,以及堆栈不是无限资源的事实,通常使我倾向于仅在我知道它可以达到的深度有限制的情况下使用递归

例如,我知道一个平衡的二叉树搜索只会深入到50层,搜索1万亿个条目。但是,我不会使用:

def sum1through (n):
    if n == 0 return 0
    return n + sum1through (n-1)

因为对两千万的
n
这样做对于一个堆栈来说是不健康的。

这个问题仍然存在。递归占用大量堆栈空间,因为每次方法调用自身时,都会再次生成指向它及其局部变量的指针。递归过程中的函数调用次数决定了O(n)内存使用率;与非递归函数(如循环)的O(1)相比。

如果递归函数的内核的计算成本低于函数入口/出口代码和调用本身的成本,则递归可能会导致显著的开销。最好的方法是简单地分析两个版本的代码——一个是递归的,另一个不是

这就是说,如果您避免递归的想法是自己创建一个类似堆栈的结构,请注意——它不一定比更直接的递归方法快。同样,分析是你的朋友


最后,请记住,程序员时间比CPU时间更昂贵。在对代码进行微优化之前,最好先衡量一下它是否真的会成为一个问题。

人们对性能说了很多愚蠢的话

  • 如果您需要递归,比如深度优先树遍历,那么您需要它,所以使用它

  • 在担心任何事情的性能之前,先找出您是否有问题以及问题所在。
    性能问题就像骗子和骗子——他们擅长于你最意想不到的地方,所以如果你担心递归之类的特定问题,你几乎肯定会担心错误的事情

  • 在我看来,发现性能问题的最佳方法是在挂钟时间进行堆栈采样,而不仅仅是获取测量值并了解它们的含义


    也就是说,如果你发现有10%或更多的时间用于递归调用,而在递归例程中没有发生任何其他事情,你可以循环它,那么循环它可能会有所帮助。

    gcc确实会跟踪递归优化。(参见示例)@nos:rewred-希望现在更正确。。。。除非您使用的是尾部递归,并且语言/编译器/平台能够理解。@Freed,是的,但是我认为尾部递归是编写循环的另一种方式。这还取决于实际内存使用情况和递归深度。对于已知的有界递归深度,这通常不是问题,因为在这种情况下,堆栈空间不需要任何成本。@Amigable-它们的等价物,当然,但这并不意味着编译器足够聪明,可以这样对待它们。@Konrad,堆栈空间在已经分配的意义上可能不需要任何成本,但是仍然需要更多的物理内存。此外,在堆栈上推送参数、保存寄存器以及其他任何需要执行的操作都不是免费的。请注意,编译器现在有时可以将递归转换为简单的迭代循环,即使函数不是尾部递归。看,如果有人编写了一个比CPU自己的调用堆栈更高效的堆栈数据结构,我会非常惊讶的…!当然,使用手动堆栈而不是依赖调用堆栈可能还有其他原因,但性能永远不应该是其中之一。@Konrad,已经完成了,请参阅例如glibc的qsort()实现-也就是说,要打败编译器并不容易,一个天真的尝试可能弊大于利。函数调用开销在不同的系统中可能会有很大的差异,所以这个问题只有在特定的环境下才有意义。也就是说,我认为t