在Racket中进行尾调用优化

时间:2012-07-16 16:58:46

标签: garbage-collection scheme racket tail-call-optimization

我正在做SICP exercise 2.28并偶然发现以下代码的奇怪行为:

(define (fringe tree)
  (cond
    ((null? tree) '())
    ((not (pair? tree)) (list tree))
    (else (append (fringe (car tree)) (fringe (cdr tree))))))

(define (fringe-tail tree)
  (define (fringe-iter tree result)
    (cond
      ((null? tree) result)
      ((not (pair? tree)) (list tree))
      (else (fringe-iter (cdr tree) (append result (fringe-tail (car tree)))))))
  (fringe-iter tree '()))

(define x (make-list (expt 10 4) 4))
(time (fringe x))
(time (fringe-tail x))

普通fringe的运行速度比其迭代版本fringe-tail快得多:

cpu time: 4 real time: 2 gc time: 0

VS。

cpu time: 1063 real time: 1071 gc time: 191

看起来fringe被优化为循环并避免任何分配,而fringe-tail运行速度慢得多,花费时间创建和销毁对象。

任何人都可以向我解释这个吗? (以防我使用球拍5.2.1)

2 个答案:

答案 0 :(得分:6)

如果用以下内容替换最后一个子句:

(else (fringe-iter (cdr tree) (append (fringe-tail (car tree)) result)))

然后它们以相同的速度运行该输入,并且尾递归版本对于更大的输入更快。

问题在于你append在前面的cdr列出了更长的列表,它的遍历和分配远远超过了天真的版本,后者附加了{的边缘。 {1}}在前面。

答案 1 :(得分:4)

给定代码的应用程序处于非尾部位置,因此该函数不是迭代的,尽管它的名称。 :)

试试这个:

(define (fringe-tail tree)
  (define (iter tree k)
    (cond
      [(null? tree)
       (k '())]
      [(not (pair? tree)) 
       (k (list tree))]
      [else
       (iter (car tree)
             (lambda (v1)
               (iter (cdr tree)
                     (lambda (v2)
                       (k (append v1 v2))))))]))
  (iter tree (lambda (a-fringe) a-fringe)))

但是,它仍然使用 append ,它与第一个参数的长度一样昂贵。某些退化到 fringe fringe-tail 的输入会导致很多计算上的痛苦。

让我们举一个这种退化输入的例子:

(define (build-evil-struct n)
  (if (= n 0)
      (list 0)
      (list (list (build-evil-struct (sub1 n)))
            (build-evil-struct (sub1 n))
            (list n))))

(define evil-struct (build-evil-struct 20))

当应用于 fringe fringe-iter 时,你会看到非常糟糕的表现:我在我自己的系统上观察边缘的计算时间秒 fringe-tail 。这些测试在DrRacket下运行,禁用调试。如果启用调试,您的数字将会有很大差异。

> (time (void (fringe evil-struct)))
cpu time: 2600 real time: 2602 gc time: 1212

> (time (void (fringe-tail evil-struct)))
cpu time: 4156 real time: 4155 gc time: 2740

使用这两种方法,使用追加是使这些易受某些退化输入影响的原因。如果我们编写 fringe 的累积版本,我们就可以消除这个成本,因为我们可以使用常量 cons 操作:

(define (fringe/acc tree)
  (define (iter tree acc)
    (cond [(null? tree)
           acc]
          [(not (pair? tree))
           (cons tree acc)]
          [else
           (iter (car tree) (iter (cdr tree) acc))]))
  (iter tree '()))

让我们来看看这个结构上的fringe / acc的表现:

> (time (void (fringe/acc evil-struct)))
cpu time: 272 real time: 274 gc time: 92

好多了!将所有调用转到尾调用是一件简单的事。

(define (fringe/acc/tail tree)
  (define (iter tree acc k)
    (cond [(null? tree)
           (k acc)]
          [(not (pair? tree))
           (k (cons tree acc))]
          [else
           (iter (cdr tree) acc
                 (lambda (v1)
                   (iter (car tree) v1 k)))]))
  (iter tree '() (lambda (v) v)))

> (time (void (fringe/acc/tail evil-struct)))
cpu time: 488 real time: 488 gc time: 280

在这种特殊情况下,Racket的堆栈实现比我们在延续中表示的实现堆栈快一点,因此 fringe / acc fringe / acc更快/尾。不过,这两个都明显优于 fringe 因为它们避免追加

所有这一切都说:这个功能已作为flatten函数内置到Racket中!因此,如果您不想重新发明轮子,也可以使用它。 :)