【问题标题】:Why is the recursive function performing better than the iterative function in elisp?为什么在 elisp 中递归函数的性能比迭代函数好?
【发布时间】:2017-08-05 04:50:47
【问题描述】:

作为我的一门课的测试,我们的老师要求我们测试著名的欧几里得算法的递归和非递归方法:

迭代

(defun gcdi (a b)
  (let ((x a) (y b) r)
    (while (not (zerop y))
      (setq r (mod x y) x y y r))
     x))

递归

(defun gcdr (a b)
  (if (zerop b)
     a
     (gcdr b (mod a b))))

然后我进行了测试:

(defun test-iterative ()
(setq start (float-time))
   (loop for x from 1 to 100000
     do (gcdi 14472334024676221 8944394323791464)) ; Fibonacci Numbers close to 2^64 >:)
 (- (float-time) start))

(defun test-recursive ()
(setq start (float-time))
   (loop for x from 1 to 100000
     do (gcdr 14472334024676221 8944394323791464)) ; Fibonacci Numbers close to 2^64 >:)
 (- (float-time) start))

然后我运行了计时器:

(test-recursive)

:1.359128475189209

(test-iterative)

:1.7059495449066162

所以我的问题是,为什么递归测试的执行速度比迭代测试快?迭代几乎总是比递归更好吗? elisp 是个例外吗?

【问题讨论】:

    标签: recursion lisp elisp


    【解决方案1】:

    理论上的答案是递归版本其实是tail 递归的,因此应该编译成迭代。

    但是,disassembling 函数揭示了真相:

    byte code for gcdi:
      args: (a b)
    0       varref    a
    1       varref    b
    2       constant  nil
    3       varbind   r
    4       varbind   y
    5       varbind   x
    6       varref    y
    7:1     constant  0
    8       eqlsign   
    9       goto-if-not-nil 2
    12      constant  mod
    13      varref    x
    14      varref    y
    15      call      2
    16      varset    r
    17      varref    y
    18      varset    x
    19      varref    r
    20      dup       
    21      varset    y
    22      goto      1
    25:2    varref    x
    26      unbind    3
    27      return    
    

    byte code for gcdr:
      args: (a b)
    0       varref    b
    1       constant  0
    2       eqlsign   
    3       goto-if-nil 1
    6       varref    a
    7       return    
    8:1     constant  gcdr
    9       varref    b
    10      constant  mod
    11      varref    a
    12      varref    b
    13      call      2
    14      call      2
    15      return    
    

    您可以看到 gcdr 几乎有 一半 的指令,但包含 两个 call 指令,这意味着 ELisp 没有不 显然,将尾递归调用转换为迭代。 然而,ELisp 中的函数调用相对便宜且 因此递归版本执行得更快。

    附言。虽然这个问题是有道理的,而且答案实际上是普遍适用的(例如,相同的方法对 Python 和 CLISP 等都有效),但应该注意选择正确的算法(例如,线性合并排序而不是二次气泡-sort) 比实现的“微优化”重要得多。

    【讨论】:

    • @tbf:谢谢,已修复。我实际上盯着call 的两条线,不知道它们是什么意思:-(
    • 请注意,由于尾部调用消除,它并没有更快,因为这里没有尾部调用消除:您可以在字节码中看到递归调用(从 8 开始,它是 push 'gcdr on the stack, push b, push 'mod 入栈, push a, push b, call 2 arg fn (即 mod ab), call 2 arg fn (即 (gcdr b ))) .它之所以快是因为 elisp 中的代码少得多,并且函数调用相对于其他操作来说速度更快。
    • 删除了我之前的评论,但留下了一个描述字节码的评论,因为我认为这是值得了解的。
    【解决方案2】:

    嗯...确实很奇怪,因为 Emacs 的函数调用(以及递归)的实现效率不高。

    我刚刚评估了下面的代码:

    (defun sm-gcdi (a b)
      (let ((x a) (y b) r)
        (while (not (zerop y))
          (setq r (mod x y) x y y r))
        x))
    (defun sm-gcdr (a b)
      (if (zerop b)
          a
        (sm-gcdr b (mod a b))))
    
    (defun sm-test-iterative ()
      (let ((start (float-time)))
        (dotimes (_ 100000)
          (sm-gcdi 14472334024676221 8944394323791464))
        (- (float-time) start)))
    
    (defun sm-test-recursive ()
      (let ((start (float-time)))
        (dotimes (_ 100000)
          (sm-gcdr 14472334024676221 8944394323791464))
        (- (float-time) start)))
    

    然后尝试了M-: (sm-test-recursive)M-: (sm-test-iterative),果然迭代版本对我来说更快。然后我又做了M-: (byte-compile 'sm-gcdi)M-: (byte-compile 'sm-gcdr)又试了一遍,速度差距更大了。

    所以你的测量结果让我感到惊讶:它们不符合我的期望,也不符合我的测试。

    【讨论】:

    • 我也可以用你的食谱复制 Cameron Fife 的原始结果——递归版本更快。 2017-02-07 的 GNU Emacs 25.2.1(x86_64-unknown-linux-gnu、X 工具包、Xaw 滚动条)。 emacs -Q 加上 (require 'cl) 用于 loop 代码。
    猜你喜欢
    • 2020-01-09
    • 2019-05-03
    • 1970-01-01
    • 2012-01-26
    • 2021-12-31
    • 1970-01-01
    • 2020-08-08
    • 2015-05-30
    相关资源
    最近更新 更多