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

Clojure中优化尾部递归:指数移动平均,clojure,tail-recursion,Clojure,Tail Recursion,我是Clojure的新手,尝试使用尾部递归实现指数移动平均函数。在使用lazy-seq和concat与堆栈溢出进行了一些斗争之后,我得到了下面的实现,它工作正常,但速度非常慢: (defn ema3 [c a] (loop [ct (rest c) res [(first c)]] (if (= (count ct) 0) res (recur (rest ct)

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

(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)))]
                    )
                )
            )
        )
    )
对于10000项集合,Clojure大约需要1300ms,而Python调用

s.ewm(alpha=0.3, adjust=True).mean()
只需要700美元。我如何才能缩小这种性能差距?谢谢,

如果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"
(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"
使用peek:

这是一个使用reduce的版本,在我的计算机上速度更快:

(time (ema3 (range 10000) 0.3))
"Elapsed time: 990.417668 msecs"
(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"
对这些时间安排持怀疑态度。使用类似的方法进行更彻底的基准测试。使用可变/瞬变,您可能能够挤出更多的增益。

如果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"
(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"
使用peek:

这是一个使用reduce的版本,在我的计算机上速度更快:

(time (ema3 (range 10000) 0.3))
"Elapsed time: 990.417668 msecs"
(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"

对这些时间安排持怀疑态度。使用类似的方法进行更彻底的基准测试。你也许可以利用易变性/瞬变性挤出更多的收益。

就我个人而言,我会懒散地减少收益。这比使用循环/递归或使用reduce手动构建结果向量更简单,也意味着您可以在构建结果时使用它,而不需要等待最后一个元素完成后再查看第一个元素

如果您最关心吞吐量,那么我认为taylorwood的reduce是最好的方法,但是lazy解决方案只会稍微慢一点,并且更加灵活

(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要使用double,那么也可以加快速度,这样就不必重复检查a、c等类型

(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解决方案中使用原语或者使用loop/recur来挤出更多。这在循环中尤其有用,因为您不必一直对double和double之间的中间结果进行装箱和拆箱操作。

就我个人而言,我会懒洋洋地进行简化。这比使用循环/递归或使用reduce手动构建结果向量更简单,也意味着您可以在构建结果时使用它,而不需要等待最后一个元素完成后再查看第一个元素

如果您最关心吞吐量,那么我认为taylorwood的reduce是最好的方法,但是lazy解决方案只会稍微慢一点,并且更加灵活

(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要使用double,那么也可以加快速度,这样就不必重复检查a、c等类型

(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解决方案中使用原语或者使用loop/recur来挤出更多。这在循环中尤其有用,因为您不必一直对double和double之间的中间结果进行装箱和拆箱。

谢谢!使用peek而不是last,速度提高了100倍,这太神奇了!我们也会看看减少选项!删除对计数的调用与peek/last更改一样是一个巨大的加速。计算一个惰性序列是非常昂贵的,如果你只关心它是否为空,你应该使用seq来代替。谢谢!使用peek而不是last,速度提高了100倍,这太神奇了!我们也会看看减少选项!删除对计数的调用与peek/last更改一样是一个巨大的加速。计算一个惰性序列是非常昂贵的,如果你只关心它是否为空,那么你应该使用seq。我也更喜欢更懒的缩减方法。我也更喜欢更懒的缩减方法。