我有以下Clojure代码来计算具有某种“可以考虑的”属性的数字。 (代码究竟做的是次要的)。
(defn factor-9
([]
(let [digits (take 9 (iterate #(inc %) 1))
nums (map (fn [x] ,(Integer. (apply str x))) (permutations digits))]
(some (fn [x] (and (factor-9 x) x)) nums)))
([n]
(or
(= 1 (count (str n)))
(and (divisible-by-length n) (factor-9 (quot n 10))))))
现在,我进入TCO并意识到Clojure只能在使用recur
关键字明确告知后才提供尾递归。所以我重写了代码来做到这一点(用recur代替因子-9是唯一的区别):
(defn factor-9
([]
(let [digits (take 9 (iterate #(inc %) 1))
nums (map (fn [x] ,(Integer. (apply str x))) (permutations digits))]
(some (fn [x] (and (factor-9 x) x)) nums)))
([n]
(or
(= 1 (count (str n)))
(and (divisible-by-length n) (recur (quot n 10))))))
据我所知,TCO有双重好处。第一个是它不像非尾递归调用那样使用堆栈,因此不会在更大的递归上使用堆栈。第二,我认为因此它可以更快,因为它可以转换为循环。
现在,我已经做了一个非常粗略的基准测试,但两个实现之间没有看到任何差异。我的第二个假设是错误的还是这与在JVM上运行(没有自动TCO)和recur
使用技巧来实现它有关?
谢谢。
答案 0 :(得分:6)
使用recur可以加快速度,但在递归调用时只需要大约3纳秒(真的)。当事情变得那么小时,它们就会隐藏在其余测试的噪音中。我写了四个测试(下面的链接),它们能够说明性能上的差异。
我还建议在基准测试时使用类似标准的东西。 (Stack Overflow不会让我发布超过1个链接因为我没有名气可言,所以你必须谷歌它,也许“clojure标准”)
出于格式化原因,我已将测试和结果放入此gist。
简而言之,相对比较,如果递归测试为1,则循环测试约为0.45,TCO测试约为0.87,递归和TCO测试之间的绝对差值约为3ns。
当然,所有关于基准测试的警告都适用。
答案 1 :(得分:2)
在优化任何代码时,最好从潜在或实际的瓶颈开始并优先考虑。
在我看来,这段特殊代码占用了大部分CPU时间:
(map (fn [x] ,(Integer. (apply str x))) (permutations digits))
这并不以任何方式依赖于TCO - 它以相同的方式执行。因此,在这个特定的例子中,tail调用将允许你不用尽所有堆栈,但为了获得更好的性能,请尝试优化它。
答案 2 :(得分:1)
只是一个异教徒提醒,clojure没有TCO
答案 3 :(得分:-1)
在评估factor-9 (quot n 10)
之后,and
和or
必须在函数返回之前进行评估。因此它不是尾递归的。