优化Clojure中的尾递归:指数移动平均值

时间:2017-10-26 20:46:34

标签: clojure tail-recursion

我是Clojure的新手并尝试使用尾递归实现指数移动平均函数。在使用lazy-seq和concat进行了一些堆栈溢出后,我得到了以下实现,但是速度非常慢:

distanceTo

对于10,000个项目集合,Clojure将花费大约1300ms,而Python Pandas调用如

(defn ema3 [c a]
    (loop [ct (rest c) res [(first c)]]
        (if (= (count ct) 0)
            res
            (recur
                (rest ct)
                (into;NOT LAZY-SEQ OR CONCAT
                    res
                    [(+ (* a (first ct)) (* (- 1 a) (last res)))]
                    )
                )
            )
        )
    )

只需700美元。如何减少性能差距?谢谢,

2 个答案:

答案 0 :(得分:3)

如果res是一个向量(在您的示例中),那么使用peek代替last会产生更好的效果:

(defn ema3 [c a]
  (loop [ct (rest c) res [(first c)]]
    (if (= (count ct) 0)
      res
      (recur
        (rest ct)
        (into
          res
          [(+ (* a (first ct)) (* (- 1 a) (peek res)))])))))

我的电脑上的例子:

(time (ema3 (range 10000) 0.3))
"Elapsed time: 990.417668 msecs"

使用peek

(time (ema3 (range 10000) 0.3))
"Elapsed time: 9.736761 msecs"

此处使用reduce的版本在我的计算机上速度更快:

(defn ema3 [c a]
  (reduce (fn [res ct]
            (conj
              res
              (+ (* a ct)
                 (* (- 1 a) (peek res)))))
          [(first c)]
          (rest c)))
;; "Elapsed time: 0.98824 msecs"

采取这些时间与一粒盐。使用类似criterium的内容进行更彻底的基准测试。您可以使用可变性/瞬态来挤出更多的收益。

答案 1 :(得分:3)

就个人而言,我会用reductions懒散地做这件事。比使用循环/重复或用reduce手动构建结果向量更简单,这也意味着您可以在构建时使用结果,而不是需要等待在你看第一个元素之前要完成的最后一个元素。

如果你最关心吞吐量,那么我认为泰勒伍德的reduce是最好的方法,但懒惰的解决方案只是稍微慢一点,而且更灵活。

(defn ema3-reductions [c a]
  (let [a' (- 1 a)]
    (reductions
     (fn [ave x]
       (+ (* a x)
          (* (- 1 a') ave)))
     (first c)
     (rest c))))

user> (quick-bench (dorun (ema3-reductions (range 10000) 0.3)))

Evaluation count : 288 in 6 samples of 48 calls.
             Execution time mean : 2.336732 ms
    Execution time std-deviation : 282.205842 µs
   Execution time lower quantile : 2.125654 ms ( 2.5%)
   Execution time upper quantile : 2.686204 ms (97.5%)
                   Overhead used : 8.637601 ns
nil
user> (quick-bench (dorun (ema3-reduce (range 10000) 0.3)))
Evaluation count : 270 in 6 samples of 45 calls.
             Execution time mean : 2.357937 ms
    Execution time std-deviation : 26.934956 µs
   Execution time lower quantile : 2.311448 ms ( 2.5%)
   Execution time upper quantile : 2.381077 ms (97.5%)
                   Overhead used : 8.637601 ns
nil

老实说,在那个基准测试中你甚至不能告诉懒人版本比矢量版本慢。我认为我的版本仍然较慢,但这是一个微不足道的差异。

如果你告诉Clojure期望双打,你也可以加快速度,所以它不必仔细检查ac的类型,等等。

(defn ema3-reductions-prim [c ^double a]
  (let [a' (- 1.0 a)]
    (reductions (fn [ave x]
                  (+ (* a (double x))
                     (* a' (double ave))))
                (first c)
                (rest c))))

user> (quick-bench (dorun (ema3-reductions-prim (range 10000) 0.3)))
Evaluation count : 432 in 6 samples of 72 calls.
             Execution time mean : 1.720125 ms
    Execution time std-deviation : 385.880730 µs
   Execution time lower quantile : 1.354539 ms ( 2.5%)
   Execution time upper quantile : 2.141612 ms (97.5%)
                   Overhead used : 8.637601 ns
nil

另外25%的加速,还不错。我希望你可以在reduce解决方案中使用原语,或者如果你真的很绝望,可以使用循环/重复来缩短一点。它在循环中特别有用,因为您不必在doubleDouble之间保持装箱和拆箱中间结果。