【问题标题】:Optimize tail-recursion in Clojure: exponential moving average优化 Clojure 中的尾递归:指数移动平均线
【发布时间】:2017-10-26 20:46:34
【问题描述】:

我是 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)))]
                    )
                )
            )
        )
    )

对于 10,000 个项目的集合,Clojure 大约需要 1300 毫秒,而 Python Pandas 调用如

s.ewm(alpha=0.3, adjust=True).mean()

只需要 700 我们。我怎样才能减少这种性能差距?谢谢,

【问题讨论】:

    标签: clojure tail-recursion


    【解决方案1】:

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

    如果您最关心吞吐量,那么我认为 Taylor Wood 的 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 之间继续装箱和拆箱。

    【讨论】:

    • 我也更喜欢懒惰的reductions 方法。
    【解决方案2】:

    如果 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 之类的东西进行更彻底的基准测试。您也许可以使用可变性/瞬态来获得更多收益。

    【讨论】:

    • 谢谢!使用 peek 而不是 last 可以提高 100 倍的速度,这太神奇了!也会看看 reduce 选项!
    • 删除对count 的调用与peek/last 更改一样大。计算惰性序列非常昂贵,如果您只关心它是否为空,则应该使用seq
    猜你喜欢
    • 2019-02-16
    • 2022-01-26
    • 1970-01-01
    • 1970-01-01
    • 2017-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多