Scheme 递归与累加器风格的性能比较

Scheme 递归与累加器风格的性能比较,scheme,racket,htdp,Scheme,Racket,Htdp,我们有两个函数计算给定数的阶乘。第一个,,使用累加器样式。第二个是fact,使用自然递归 (define (! n0) (local (;; accumulator is the product of all natural numbers in [n0, n) (define (!-a n accumulator) (cond [(zero? n) accumulator] [else (!-a (sub1 n) (* n

我们有两个函数计算给定数的阶乘。第一个,
,使用累加器样式。第二个是
fact
,使用自然递归

(define (! n0)
  (local (;; accumulator is the product of all natural numbers in [n0, n)
      (define (!-a n accumulator)
        (cond
          [(zero? n) accumulator]
          [else (!-a (sub1 n) (* n accumulator))])))
    (!-a n0 1)))

在第31节的底部,声明自然递归版本通常与累加器版本一样快,但没有说明原因。我对此做了一些阅读,答案似乎是,但维基百科的文章似乎与HtDP所说的不一致,至少在性能方面是如此。为什么会这样


在工作中,递归样式更快。在家里,累加器式更快。对于哪种风格通常是首选的,是否没有一般的启发来指导选择?我知道累加器风格的内存效率更高,但如果我们将讨论仅限于性能,至少对我来说,还不清楚哪个是更好的选择



我对这一点考虑得更深入了一点,我不得不站在维基百科关于累加器式递归在一般情况下的优越性的文章一边。它不仅减少了堆栈/堆空间的使用,而且内存访问总是会落后于寄存器访问,而且只有在多核出现之后才能变得更加明显。尽管如此,HtDP证明了在所有情况下都需要实际测试。

我不知道Racket编译器的内部,但我会推测


尾部调用通常比普通调用更昂贵(在.NET中是这样,速度慢7倍),但在某些情况下,尾部调用可以被消除,并且它最终成为
,而(1){…}
C样式的循环,因此不需要进行额外调用,只需简单的本地跳转,有效地消除了过程应用程序开销。

一个好的编译器会将递归fac转换为尾部递归fac。所以编译代码应该没有区别。

答案将取决于球拍系统的细节。这是我的看法

自然递归版本和累加器版本之间有两个主要区别。首先,累加器版本以允许尾部调用优化的方式编写。这有助于加快累加器版本,因为需要创建的堆栈帧更少。但这与HtDP中讨论的内容以及您在工作计算机上看到的内容相反

另一个区别是乘法的顺序。自然递归版本按升序将数字从1乘以20,即

((((1 * 2) * 3) * … * 19) * 20)
((((20 * 19) * 18) * … * 2) * 1)
累加器版本按降序乘以相同的数字,即

((((1 * 2) * 3) * … * 19) * 20)
((((20 * 19) * 18) * … * 2) * 1)
从数学上讲,它们是相同的,两个阶乘函数将给出相同的结果。尽管如此,这一差异可能很重要。特别是,在任何中间乘法中,后一次计算的中间结果将大于前一次计算的中间结果

20的阶乘是个大数字。它不适合32位整数。这意味着racket需要使用任意长度的整数(“bignum”)来表示答案,以及一些中间结果。任意精度算法,包括包含bignum的乘法,比固定精度算法慢

由于累加器版本中的中间结果总是大于自然递归版本,因此累加器版本将需要比递归版本早的bignum。简而言之,虽然两个版本都需要相同数量的乘法,但累加器版本需要更多的任意精度乘法。这会使累加器版本变慢。显然,该算法的额外成本超过了减少堆栈帧数所节省的成本

那么为什么同样的趋势不会出现在你的家用电脑上呢?你说它是Intel iMac,所以它可能是64位系统。而20岁!是一个大数字,与64位整数相比,它是一个小数字,因此您的家用计算机不会执行任何任意精度的算术运算,并且顺序也无关紧要。HtDP已经足够老了,它将使用32位系统,就像您工作计算机上的Windows XP一样

探索差异更有用的是一个计算数字列表乘积的函数

(define (product numlist)
  (* (car numlist) (product (cdr numlist)))

或者是累加器版本。然后,您可以按升序或降序输入数字,这与您使用的是自然递归还是基于累加器的方法无关

上面有很多优点。我喜欢分析应该做什么和为什么不做。这正是欧拉项目成功的原因。太早从fixnums出发可能会有问题

数字序列可以从大到小相乘,反之亦然。我们还有直接和类似地执行迭代的“do”命令

(定义(事实n)(如果(=n1)1(*n(事实(-n1щ)))
(定义(fact1n)(do([nn(-n1)][p1(*pn)])(=n1)p)))
(定义(fact2n)(do([i1(+i1)][p1(*pi)])((
Hello leppie。。。谢谢你的回答。我在我的家用电脑(一台英特尔酷睿2双核iMac)上运行了同样的代码,有趣的是,累加器版本的计时效果始终如一,这与工作中的Windows XP PC相反。DrRacket v5.0在家中,明天将在工作中找到版本。@Greenhorn:请注意,这些版本无法比较。后者是一个非常幼稚的实现。前者将导致更少的“递归”调用,即使没有尾部调用消除。尝试以递归调用不在尾部的方式重写第一个。这可能很难,这取决于Racket中的优化器有多好。@leppie。。。这里工作的DrRacket是v.5.0.2。情况与国内的iMac相反。我和