我正在做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)
答案 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中!因此,如果您不想重新发明轮子,也可以使用它。 :)