【问题标题】:Efficiency: recursion vs loop效率:递归与循环
【发布时间】:2012-03-12 06:48:40
【问题描述】:

这只是我的好奇心,但是递归或循环哪个更有效?

给定两个函数(使用通用 lisp):

(defun factorial_recursion (x)
    (if (> x 0)
        (* x (factorial_recursion (decf x)))
        1))

(defun factorial_loop (x)
    (loop for i from 1 to x for result = 1 then
        (* result i) finally
        (return result)))

哪个更有效率?

【问题讨论】:

  • 如果你的函数是尾递归的,它基本上与循环相同。尾递归可以优化为一个简单的循环,使它们相同。不过,您的函数不是尾递归的。
  • @Gabe,虽然尾递归可以优化为循环,但值得注意的是 Common Lisp 实现不需要优化尾调用,尽管许多实现都这样做。

标签: performance recursion common-lisp


【解决方案1】:

我什至不必阅读您的代码。

循环对阶乘更有效。当您进行递归时,堆栈上最多有 x 个函数调用。

出于性能原因,您几乎从不使用递归。您使用递归使问题更简单。

【讨论】:

  • 这在大多数情况下是正确的,但recursion 引入了更简单的推理,如果您的编译器支持尾调用优化,那么它可能仍然与iterative 函数一样快,因为recursive 函数然后将在编译时转换为循环。因此,您拥有迭代函数的速度和递归函数的轻松推理能力。
  • @AdrianLegaspi 即便如此,这是否意味着迭代效率更高?
  • @theX 它是一个case-to-case的基础,效率更高,一些操作在堆栈中更快,一些操作在迭代中更快。在这种情况下,单点比较偏向于递归和迭代的用例;迭代要快得多。从这个意义上说,这也是一种语言如何处理代码的问题,正如我已经提到的,一些编译器根据其对该代码的计算将递归转换为其二进制文件的循环。例如GHC
【解决方案2】:

亩。

现在说真的,没关系。不是这个尺寸的例子。它们都具有相同的复杂性。如果您的代码对您来说不够快,这可能是您最后要看的地方之一。

现在,如果您真的想知道哪个更快,请测量它们。在 SBCL 上,您可以循环调用每个函数并测量时间。既然你有两个简单的功能,time 就足够了。如果您的程序更复杂,profiler 会更有用。提示:如果您的测量不需要分析器,您可能不需要担心性能。

在我的机器(SBCL 64 位)上,我运行了你的函数并得到了这个:

CL-USER> (time (loop repeat 1000 do (factorial_recursion 1000)))
Evaluation took:
  0.540 seconds of real time
  0.536034 seconds of total run time (0.496031 user, 0.040003 system)
  [ Run times consist of 0.096 seconds GC time, and 0.441 seconds non-GC time. ]
  99.26% CPU
  1,006,632,438 processor cycles
  511,315,904 bytes consed

NIL
CL-USER> (time (loop repeat 1000 do (factorial_loop 1000)))
Evaluation took:
  0.485 seconds of real time
  0.488030 seconds of total run time (0.488030 user, 0.000000 system)
  [ Run times consist of 0.072 seconds GC time, and 0.417 seconds non-GC time. ]
  100.62% CPU
  902,043,247 processor cycles
  511,322,400 bytes consed

NIL

将函数放入顶部带有(declaim (optimize speed)) 的文件后,递归时间降至 504 毫秒,循环时间降至 475 毫秒。

如果您真的想知道发生了什么,请在您的函数上尝试dissasemble,看看里面有什么。

再一次,这对我来说似乎不是问题。就个人而言,我尝试使用 Common Lisp 之类的脚本语言来进行原型设计,然后对速度较慢的部分进行分析和优化。从 500 毫秒到 475 毫秒没什么。例如,在一些个人代码中,通过简单地将元素类型添加到数组中,我获得了几个数量级的加速(因此在我的例子中使数组存储空间小了 64 倍)。当然,从理论上讲,重用该数组(在使其更小之后)而不是一遍又一遍地分配它会更快。但是对于我的情况,只需添加:element-type bit 就足够了——更多的变化需要更多的时间才能获得很少的额外收益。也许我很草率,但“快”和“慢”对我来说意义不大。我更喜欢“足够快”和“太慢”。在大多数情况下,您的两个函数都“足够快”(或者在某些情况下两者都“太慢”),因此它们之间没有真正的区别。

【讨论】:

    【解决方案3】:

    如果你可以编写递归函数,使得递归调用是最后一件事完成(因此函数是尾递归并且您使用的语言和编译器/解释器支持尾递归,那么递归函数(通常)可以优化为真正迭代的代码,并且与同一函数的迭代版本一样快。 p>

    Sam I Am 是正确的,迭代函数通常比递归函数更快。如果递归函数要与执行相同操作的迭代函数一样快,则必须依赖优化器。

    这样做的原因是函数调用比跳转更昂贵,而且你会消耗堆栈空间,这是一种(非常)有限的资源。

    您给出的函数不是尾递归的,因为您调用了factorial_recursion,然后将其乘以x。尾递归版本的一个例子是

    (defun factorial-recursion-assist (x cur)
        (if (> x 1)
            (factorial-recursion-assist (- x 1) (+ cur (* (- x 1) x)))
            cur))
    
    (defun factorial-recursion (x)
        (factorial-recursion-assist x 1))
    
    (print (factorial-recursion 4))
    

    【讨论】:

    • Common Lisp 标准没有以任何方式提及尾递归。不过,一些 CL 编译器支持它。需要阅读他们的手册以了解如何强制编译器进行 TCO。
    • @RainerJoswig 是的,这就是为什么我在尾递归的先决条件列表中也提到了编译器/解释器
    • ...尾递归优化,即
    【解决方案4】:

    这是一个尾递归阶乘(我认为):

    (defun fact (x)
      (funcall (alambda (i ret)
                 (cond ((> i 1)
                        (self (1- i) (* ret i)))
                       (t
                        ret)))
               x 1))
    

    【讨论】:

      猜你喜欢
      • 2011-02-09
      • 1970-01-01
      • 2015-07-18
      • 2019-04-27
      • 2012-02-12
      • 2012-08-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多