【问题标题】:Recursion overhead -- how serious is it? [duplicate]递归开销——有多严重? [复制]
【发布时间】:2011-04-29 20:17:22
【问题描述】:

可能重复:
Is recursion ever faster than looping?

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

现在我用 C、Python、Perl 和 Java 编写代码,有时我想知道递归。重写它们还有什么好处吗?如果它们是尾递归怎么办?现代编译器是否已经解决了所有这些问题?这些问题与解释语言无关吗?

【问题讨论】:

  • 函数调用开销在不同系统之间可能会有很大差异,因此这个问题仅在特定上下文中才有意义。也就是说,我认为过去几十年的总体趋势是减少开销。

标签: optimization programming-languages recursion tail-recursion interpreted-language


【解决方案1】:

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

但是您需要考虑的另一件事是,计算机的速度比 15 年前快了几个数量级,因此现在您需要担心微优化的情况比以前少了很多。一个 15 年前的程序可能需要在汇编程序中进行仔细的手工优化才能获得良好的性能,即使是用 Java 等高级语言编写的,它也可能在现代计算机上运行得非常快。这并不是说性能不再是问题 - 但您应该专注于选择正确的算法和编写可读代码。只有在你测量了性能并且你可以看到有问题的代码是瓶颈之后才进行微优化。

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

【讨论】:

【解决方案2】:

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

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

例如,我知道平衡二叉树搜索对于 100 亿个条目只能进行 50 级深。但是,我不会使用:

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

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • ...除非您使用尾递归和可以理解的语言/编译器/平台。
    • @Freed,是的,但在我看来,尾递归是另一种编写循环的方式。
    • 这也取决于实际内存使用情况和递归深度。对于已知的有界递归深度,这通常不是问题,因为在这种情况下堆栈空间不会花费任何成本。
    • @Amigable - 它们的等价物,当然,但这并不意味着编译器足够聪明地对待它们。
    • @Konrad,从已经分配的意义上说,堆栈空间可能不会花费任何成本,但仍需要更多物理内存。此外,将参数压入堆栈、保存寄存器以及您需要做的任何其他事情都不是免费的。
    【解决方案4】:

    如果递归函数的内核的计算成本低于函数进入/退出代码和调用本身的成本,则递归可能会导致大量开销。找出答案的最佳方法是简单地分析代码的两个版本 - 一个是递归的,一个不是。

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

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

    【讨论】:

    • 请注意,编译器现在有时可以将递归转换为简单的迭代循环,即使函数不是尾递归的。见linux-kongress.org/2009/slides/…
    • 如果有人开始编写比 CPU 自己的调用堆栈更高效的堆栈数据结构,我会感到非常惊讶……!当然,使用手动堆栈而不是依赖调用堆栈可能还有其他原因,但性能绝不应该是其中之一。
    • @Konrad,它已经完成了,参见 glibc 的 qsort 实现 (goo.gl/6ptK) - 也就是说,击败编译器并不容易,而且天真的尝试是可能弊大于利。
    【解决方案5】:

    人们对性能说了很多傻话。

    1. 如果你需要递归,喜欢做深度优先的树遍历,那么你需要它,所以使用它。

    2. 在担心任何事情的性能之前,请先弄清楚您是否有问题以及问题出在哪里。
      性能问题就像骗子和骗子——它们专注于你最不期望的地方,所以如果你担心特定的事情,比如递归,你几乎肯定会担心错误的事情。

    在我看来,找出性能问题的最佳方法是在挂钟时间examining the samples to see what the program is doing 进行堆栈采样,而不仅仅是通过测量并弄清楚它们的含义。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-03-09
      • 2010-11-25
      • 1970-01-01
      • 2013-05-05
      • 2011-11-09
      • 1970-01-01
      相关资源
      最近更新 更多