为什么在这个例子中使用Reducer没有显着的加速?

时间:2013-05-20 16:37:25

标签: clojure parallel-processing reducers

(require '[clojure.core.reducers :as r])

(def data (into [] (take 10000000 (repeatedly #(rand-int 1000)))))

(defn frequencies [coll]
  (reduce (fn [counts x]
    (merge-with + counts {x 1}))
    {} coll))

(defn pfrequencies [coll]
  (r/reduce (fn [counts x]
    (merge-with + counts {x 1}))
    {} coll))


user=> (time (do (frequencies data) nil))
"Elapsed time: 29697.183 msecs"

user=> (time (do (pfrequencies data) nil))
"Elapsed time: 25273.794 msecs"

user=> (time (do (frequencies data) nil))
"Elapsed time: 25384.086 msecs"

user=> (time (do (pfrequencies data) nil))
"Elapsed time: 25778.502 msecs"

谁可以向我展示一个显着加速的例子?

我正在Mac OSX 10.7.5上使用Java 1.7在Intel Core i7上运行(2核,http://ark.intel.com/products/54617)。

3 个答案:

答案 0 :(得分:19)

您将其称为pfrequencies,它与问题上的parallel-processing标记一起表示您认为某些内容正在使用多个主题。情况并非如此,也不是减速器库的“主要”目标。

减速器给你带来的主要好处是你不需要为懒惰序列分配许多中间缺陷单元。在引入Reducer之前,frequencies将分配10000000个cons单元来创建reduce要使用的向量的顺序视图。现在减速器存在,矢量知道如何在不创建这样的临时对象的情况下减少自己。但该功能已被移植到clojure.core/reduce,其行为与r/reduce完全相同(忽略了一些与此无关的小功能)。因此,您只需将您的函数与其自身的相同克隆进行基准测试。

reducers库还包含fold的概念,它可以并行执行某些工作,然后将中间结果合并在一起。要使用它,您需要提供比reduce需要更多的信息:您必须定义如何从零开始“块”;你的功能必须是联想的;并且您必须指定如何组合块。 A. Webb's answer演示了如何正确使用fold,以便在多个线程上完成工作。

但是,你不可能从折叠中获得任何好处:除了他注意到的原因(你放弃了瞬态,与clojure.core/frequencies相比),构建地图不容易并行化。如果frequencies中的大部分工作都是添加的(就像(frequencies (repeat 1e6 1))那样),那么fold会有所帮助;但大多数工作都是在管理hashmap中的键,最终必须是单线程。您可以并行构建地图,但是您必须将它们合并在一起;因为该组合步骤需要与块的大小成比例的时间,而不是恒定的时间,所以无论如何通过在单独的线程上执行块来获得很少。

答案 1 :(得分:5)

频率函数的fold版本看起来像

(defn pfrequencies [coll] 
  (r/fold 
    (fn combinef
      ([] {})
      ([x y] (merge-with + x y)))
    (fn reducef
      ([counts x] (merge-with + counts {x 1})))
    coll))

在2个核心上,它可能比使用瞬变的clojure.core/frequencies慢得多。至少在4个内核上,它比第一个实现更快(2x),但仍然比clojure.core/frequencies慢。

您也可以尝试

(defn p2frequencies [coll]
  (apply merge-with + (pmap clojure.core/frequencies (partition-all 512 coll))))

答案 2 :(得分:4)

这里的答案中有一些重要的思考。在这种特定情况下,不需要映射,因为可以容易地预测结果域并将其放入可以使用索引的向量中。因此,天真问题的天真实现将是:

(defn freqs
  [coll]
  (reduce (fn [counts x] (assoc counts x (inc (get counts x))))
          (vec (int-array 1000 0))
          coll))

(defn rfreqs
     [coll]
     (r/fold
       (fn combinef
         ([] (vec (int-array 1000 0)))
         ([& cols] (apply mapv + cols)))
       (fn reducef
         [counts x] (assoc counts x (inc (get counts x))))
       coll))

这里的combinef将是对所得集合的1000列的简单地图添加,这应该可以忽略不计。

这使得reducer版本的速度比普通版本快2-3倍,特别是在较大的(10x-100x)数据集上。有些人用r / fold的分区大小(可选'n'参数)可以做微调。看似最佳使用(* 16 1024),数据大小为1E8(至少需要6GB JVM)。

你甚至可以在两个版本中使用瞬变,但我没有注意到很多改进。

我知道这个版本不适合通用,但它可能会显示速度提升,而不会产生哈希管理开销。