【问题标题】:How does using stack instead of recursion give better performance in C when recursion uses stack as well?当递归也使用堆栈时,使用堆栈而不是递归如何在 C 中提供更好的性能?
【发布时间】:2016-01-03 10:17:07
【问题描述】:

这个问题是通过学习 C 语言提出的。

我在我的数据结构课程中看到,在许多情况下,递归被证明是一种快速简便的解决方案(例如快速排序、二叉搜索树的遍历等),已经明确提到使用自创堆栈是一个更好的主意。

给出的原因是递归需要很多函数调用,而函数调用“更慢”。

但是如何证明使用自创建堆栈更好,因为任何函数调用都使用堆栈?

【问题讨论】:

  • 问得好,但你为什么不问你的导师呢?如果他不能提供明确的理由,你可以对他的知识有所了解。
  • @Olaf 也许他做到了?不过,通过在这里提问,他可以获得更广泛的答案,并能够将他的导师(如果他有的话?)给他的答案更好地结合起来。一般来说,我建议您不要使用 cmets 来对存在问题进行二次猜测,而是对问题进行改进和澄清。
  • @NathanielFord:是的,我应该添加“太”左右,因为通过将这里的答案与导师所说的(结合某种原因)进行比较,可以衡量一个人可以信任导师的程度以及他的知识实际上有多广,或者他在没有反思的情况下重复他所学的内容的速度有多快。要求第二次猜测不也是某种澄清吗?我们不是千里眼(好吧,至少我不是),所以我们不知道为什么导师这么说。
  • @Olaf 我在我用过的书中找到了这个。我不记得我的导师有没有告诉过我这件事。根据我的经验,向某人询问他们从未说过的事情通常是个坏主意,因为这会使您处于首先需要向他们解释概念的情况;)
  • @RegisteredUser:文字只是唤起了这样的印象。无论如何,您应该问他并与答案进行比较。这是获得印象的好方法。这可能是上课的好主意。正如我在第一条评论中所写:好问题!

标签: c performance recursion stack


【解决方案1】:

自创堆栈比执行堆栈更高效的真正原因有两个:

  1. 执行堆栈旨在处理调用新函数的一般情况。这意味着它有很多开销:它必须包含指向前面函数的指针,它必须包含指向堆上的值的指针,以及许多其他簿记项。如果您的计算确实是特定的,那么这可能超出您对特定计算的需要。所有额外的管理都会降低效率。在函数非常繁重且调用相对较少的情况下,这很好。在函数本身比较简单,但函数调用很多的情况下,开销成本不成比例地增加。

  2. 通用堆栈对您隐藏了很多细节,使您无法利用直接引用堆栈的不同部分。例如,堆栈的根对您隐藏。假设您正在使用递归在大树中搜索特定值。在某些时候,您在树的深处有一千个节点,并且您找到了价值。成功!但是你必须一次从树中爬出一个函数:这意味着至少有一千次调用只是为了返回值。 (*) 相反,如果您编写了自己的堆栈,则可以立即返回。或者,假设您有一个算法,在树中的某些节点上,需要您在继续执行之前备份n 堆栈帧。使用通用堆栈框架,您需要退出这些框架,直到找到您正在寻找的那个。如果你专门为你的算法设计了堆栈,你可以提供一种机制来立即跳转到一条指令中的执行点,而不是n

因此,如果您可以利用丢弃不需要但会花费时间的通用堆栈框架机制的部分,或者正在编写的算法可以利用移动的优势,那么您应该编写自己的堆栈如果它知道它在做什么,则快速通过堆栈(通用堆栈通过隐藏它的抽象来“保护”您不这样做)。请记住,函数调用只是用于处理代码的一种特殊的通用抽象:如果由于某种原因它们添加了一个使您的代码变得笨拙的约束,您可能可以创建一个更直接满足您需求的精简版本。

如果分配给您的堆栈的内存与您必须递归的次数相比较小,您也可以创建自己的堆栈,例如如果您有一个非常大的输入域或者如果您在专门的小占用空间上运行硬件或类似情况。不过,这同样取决于您正在运行的算法以及通用堆栈解决方案如何帮助或阻碍它。

(*) 尾递归通常会有所帮助,但由于尾递归根据定义仅进入更深一层的堆栈帧,我假设您所讨论的情况并非严格意义上的可能。

【讨论】:

    【解决方案2】:

    通常,函数调用在函数内部完成任何操作之前都会产生一些开销。为函数调用生成的代码基本上可以确保当您返回时,您会找到与您离开时一样的所有内容;同时它在被调用函数内为您提供了一个干净的空环境。事实上,这种便利性是 C 提供的最重要的服务之一,仅次于标准库。 (在许多其他方面,C 只是一个宏汇编器——您是否曾经并排查看过 C 源代码和生成的汇编器?)。

    特别是通常必须保存一些寄存器,并且可能必须将参数复制到调用堆栈上。所需的工作量取决于处理器、编译器和调用约定。例如,参数和返回值可能在寄存器中,而不是在堆栈中(但是对于每个递归调用,无论如何都必须保存参数,不是吗?)。

    函数小,开销相对较大;这就是为什么内联可以很强大。内联递归函数调用类似于循环展开。我不知道当前的编译器是否定期这样做(他们可能)。但是依赖编译器是有风险的,所以如果速度很重要,我会避免琐碎函数的递归实现,比如计算阶乘。

    【讨论】:

      【解决方案3】:
      1. 自创建堆栈意味着您只需将几个重要变量推送到自创建堆栈。
      2. 使用递归时,所有变量的函数头和状态都会被压入堆栈。

      如果递归的深度很高,使用 case 2 意味着内存将很快耗尽。

      【讨论】:

        【解决方案4】:

        出于性能原因,不一定推荐使用自创堆栈。我能想到的一个很好的理由是“常规”堆栈可能具有固定大小(通常为 1MB),因此例如对大量数据进行排序会导致堆栈溢出

        【讨论】:

          猜你喜欢
          • 2015-09-12
          • 2021-02-04
          • 2014-12-10
          • 2018-10-03
          • 1970-01-01
          • 2017-01-20
          • 1970-01-01
          • 2013-08-27
          • 2021-01-21
          相关资源
          最近更新 更多