【问题标题】:Tuning clojure reducers library performance调整 clojure reducers 库性能
【发布时间】:2014-03-28 17:12:16
【问题描述】:

为什么使用 reducers 库进行映射/归约的性能比普通的 map/reduce 差?

user=> (time (reduce + (map inc (range 100000000))))
"Elapsed time: 14412.627966 msecs"
5000000050000000

user=> (time (reduce + (r/map inc (range 100000000))))
... (C-c)

user=> (time (r/reduce + (r/map inc (range 100000000))))
....(C-c)

我有两个杀死后两个,因为它需要无限长的时间。这里有什么问题?

编辑: 似乎其他语言也有类似的问题。 Scala 似乎只突破了一百万。 Why do Scala parallel collections sometimes cause an OutOfMemoryError? 。虽然 clojure reducer 比正常速度快一百万。

【问题讨论】:

标签: clojure reducers


【解决方案1】:

由于内存耗尽,性能正在停止。如果您继续等待,您很可能会遇到内存错误。创建一个 reducer 将集合的头部保存在一个闭包中。因此,巨大的惰性序列在实现时会占用内存。

这是正在发生的事情,提炼

user=> (def n 100000000)
#'user/n

user=> (time (dorun (range n)))
"Elapsed time: 12349.034702 msecs"

现在相同,但在闭包内

user=> (defn foo [x] (fn [f] (f x)))
#'user/foo

user=> (time ((foo (range n)) dorun))
OutOfMemoryError GC overhead limit exceeded ... (sometime later)

比较

(time (do (doall (range n)) nil))
OutOfMemoryError GC overhead limit exceeded ... (sometime later)

reducers 中的可疑闭包

user=> (source r/folder)
(defn folder
  "Given a foldable collection, [...]"
  {:added "1.5"}
  ([coll xf]
     (reify
      clojure.core.protocols/CollReduce
      (coll-reduce [_ f1]
                   (clojure.core.protocols/coll-reduce coll (xf f1) (f1)))
   ...

Christophe Grand 有一个 nice post 介绍如何以惰性方式编写 reducer。

【讨论】:

  • 所以,我完全错过了后来在同一个博客 Christophe Grand 的两篇文章涵盖了这个确切的问题。我通过链接帖子遵循cgrand's answer
【解决方案2】:

Reducer 不能很好地处理惰性列表,而普通的 reduce 可以。

要从减速器中获得真正的好处,您需要一个非惰性集合,例如一个向量,你需要使用 fold 而不是 reduce。

 (def v (into [] (range 10000000)))
 (time (reduce + (map inc v)))
 ;; "Elapsed time: 896.79 msecs"
 (time (reduce + (r/map inc v)))
 ;; "Elapsed time: 671.947 msecs" 
 (time (r/fold + (r/map inc v)))
 ;; "Elapsed time: 229.45 msecs"

Reducer 使用需要大量数据的 fork/join 框架。在惰性(分块)序列中,您没有这些大块,因此 fork/join 无法正常工作。

Rich Hickey 有一个关于 reducer 的演讲,很好地解释了这些概念:https://vimeo.com/45561411

【讨论】:

  • fork/join 在这里不适用于 r/map 或 r/reduce。另外,r/reduce 和 normal reduce 是一样的。
  • 添加另一个 0 到您的问题范围内,根据您的 JVM 设置,您还可能在第一行遇到内存错误。
【解决方案3】:

为了补充@a-webb 的答案,这是一个编译器错误,需要真正修复。 (See this post for more details.)

解决此问题的另一种方法是使用保险丝

(defn once-seq
  "Returns a sequence on which seq can be called only once."
  [coll]
  (let [a (atom coll)]
    (reify clojure.lang.Seqable
      (seq [_]
        (let [coll @a]
          (reset! a nil)
          (seq coll))))))

然后:

=> (time (r/reduce + (r/map inc (once-seq (range 100000000)))))
"Elapsed time: 3692.765 msecs"
5000000050000000

【讨论】:

    猜你喜欢
    • 2016-02-01
    • 2016-03-06
    • 2023-03-28
    • 2014-11-17
    • 2011-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-25
    相关资源
    最近更新 更多