clojure lazy-seq性能优化

时间:2015-04-07 12:53:11

标签: performance clojure lazy-sequences

我是Clojure的新手,我有一些我想要优化的代码。我想计算一致性计数。 主要功能是计算空间,输出是类型

的嵌套映射
{"w1" {"w11" 10, "w12" 31, ...}
 "w2" {"w21" 14, "w22" 1,  ...}
 ... 
 }

意思是“w1”与“w11”同时发生10次等等......

它需要一系列文档(句子)和一系列目标词,它迭代两者并最终应用 context-fn ,例如 sliding-window 提取上下文单词。更具体地说,我正在通过滑动窗口

传递一个闭包
(compute-space docs (fn [target doc] (sliding-window target doc 5)) targets)

我用大约5000万个单词(约300万个句子)测试了它。 20,000个目标。这个版本需要一天多的时间才能完成。我还写了一个 pmap 并行函数( pcompute-space ),它可以将计算时间减少到大约10个小时,但我仍觉得它应该更快。我没有其他代码可以比较,但我的直觉说它应该更快。

(defn compute-space 
  ([docs context-fn targets]
    (let [space (atom {})]
      (doseq [doc docs
              target targets]
        (when-let [contexts (context-fn target doc)]
          (doseq [w contexts]
            (if (get-in @space [target w])
              (swap! space update-in [target w] (partial inc))
              (swap! space assoc-in  [target w] 1)))))
     @space)))

(defn sliding-window
  [target s n]
  (loop [todo s seen [] acc []]
    (let [curr (first todo)]
      (cond (= curr target) (recur (rest todo) (cons curr seen) (concat acc (take n seen) (take n (rest todo))))
            (empty? todo) acc
            :else (recur (rest todo) (cons curr seen) acc)))))


(defn pcompute-space
  [docs step context-fn targets]
  (reduce
     #(deep-merge-with + %1 %2)
      (pmap
        (fn [chunk]
          (do (tick))
          (compute-space chunk context-fn targets))
        (partition-all step docs)))

我用 jvisualvm 分析了应用程序,我发现clojure.lang.Cons,clojure.lang.ChunkedCons和clojure.lang.ArrayChunk正在过度控制这个过程(见图)。这肯定与我使用这个双 doseq 循环的事实有关(先前的实验表明这种方式比使用map,reduce等更快,尽管我使用时间用于对功能进行基准测试)。 我非常感谢您提供的任何见解,以及重构代码并使其运行得更快的建议。我猜 redurs 在这里可能有所帮助,但我不确定如何和/或为什么。

jvisualvm memory profile

SPECS

MacPro 2010 2.4 GHz Intel Core 2 Duo 4 GB RAM

Clojure 1.6.0

Java 1.7.0_51 Java HotSpot(TM)64位服务器VM

Test data

GithubGist with the entire code

2 个答案:

答案 0 :(得分:4)

测试数据是:

  • 42个字符串(目标)的懒惰序列
  • 懒惰的105,040懒集。 (文件)
  • Documents中的每个lazy seq都是一个懒惰的字符串序列。文件中包含的字符串总数为1,146,190。

比你的工作负荷小一点。 Criterium用于收集时间。 Criterium多次计算表达式,首先预热JIT然后收集平均数据。

使用我的测试数据和您的代码,compute-space耗时22秒:

WARNING: JVM argument TieredStopAtLevel=1 is active, and may lead to unexpected results as JIT C2 compiler may not be active. See http://www.slideshare.net/CharlesNutter/javaone-2012-jvm-jit-for-dummies.
Evaluation count : 60 in 60 samples of 1 calls.
             Execution time mean : 21.989189 sec
    Execution time std-deviation : 471.199127 ms
   Execution time lower quantile : 21.540155 sec ( 2.5%)
   Execution time upper quantile : 23.226352 sec (97.5%)
                   Overhead used : 13.353852 ns

Found 2 outliers in 60 samples (3.3333 %)
    low-severe   2 (3.3333 %)
 Variance from outliers : 9.4329 % Variance is slightly inflated by outliers

首次优化 已更新,可使用frequencies从单词矢量转换为单词映射及其出现次数。

为了帮助我理解计算的结构,我编写了一个单独的函数,它接受文档集合context-fn和单个目标,并将上下文单词的映射返回到计数。 compute-space返回的一个目标的内部地图。使用内置的Clojure函数写出来,而不是更新计数。

(defn compute-context-map-f [documents context-fn target]
  (frequencies (mapcat #(context-fn target %) documents)))

compute-context-map-f编写compute-space,名为compute-space-f here,相当简短:

(defn compute-space-f [docs context-fn targets]
  (into {} (map #(vector % (compute-context-map-f docs context-fn %)) targets)))

时间与上述数据相同,是原始版本的65%:

WARNING: JVM argument TieredStopAtLevel=1 is active, and may lead to unexpected results as JIT C2 compiler may not be active. See http://www.slideshare.net/CharlesNutter/javaone-2012-jvm-jit-for-dummies.
Evaluation count : 60 in 60 samples of 1 calls.
             Execution time mean : 14.274344 sec
    Execution time std-deviation : 345.240183 ms
   Execution time lower quantile : 13.981537 sec ( 2.5%)
   Execution time upper quantile : 15.088521 sec (97.5%)
                   Overhead used : 13.353852 ns

Found 3 outliers in 60 samples (5.0000 %)
    low-severe   1 (1.6667 %)
    low-mild     2 (3.3333 %)
 Variance from outliers : 12.5419 % Variance is moderately inflated by outliers

并行化第一次优化

我选择按目标而不是文档进行分块,因此将地图合并在一起不需要修改目标的{context-word count, ...}地图。

(defn pcompute-space-f [docs step context-fn targets]
  (into {} (pmap #(compute-space-f docs context-fn %) (partition-all step targets))))

时间与上述数据相同,是原始版本的16%:

user> (criterium.core/bench (pcompute-space-f documents 4 #(sliding-window %1 %2 5) keywords))
WARNING: JVM argument TieredStopAtLevel=1 is active, and may lead to unexpected results as JIT C2 compiler may not be active. See http://www.slideshare.net/CharlesNutter/javaone-2012-jvm-jit-for-dummies.
Evaluation count : 60 in 60 samples of 1 calls.
             Execution time mean : 3.623018 sec
    Execution time std-deviation : 83.780996 ms
   Execution time lower quantile : 3.486419 sec ( 2.5%)
   Execution time upper quantile : 3.788714 sec (97.5%)
                   Overhead used : 13.353852 ns

Found 1 outliers in 60 samples (1.6667 %)
    low-severe   1 (1.6667 %)
 Variance from outliers : 11.0038 % Variance is moderately inflated by outliers

<强>规格

  • Mac Pro 2009 2.66 GHz四核Intel Xeon,48 GB RAM。
  • Clojure 1.6.0。
  • Java 1.8.0_40 Java HotSpot(TM)64位服务器VM。

<强> TBD

进一步优化。

描述测试数据。

答案 1 :(得分:1)

分析问题中的compute-space算法

扫描句子的费用 - 寻找目标 -

  • 与单词总数成比例
  • 与目标数量成正比,但
  • 更不依赖于单词被分成的句子数量。

处理目标的成本

  • 与目标命中率成正比,
  • 与其上下文中不同单词的数量成比例。

重大改进

context-fn扫描寻找目标的句子。 如果有一万个目标,它会扫描句子一万次

更好地扫描句子一次,寻找所有目标。如果我们将目标保持为(哈希)集合,我们可以测试一个单词是否是一个或多或少恒定时间的目标,无论有多少目标

可能的改进

sliding-windows函数通过将每个单词从一个接一个地传递到另一个单词来生成上下文 - 从todoseen。将单词倒入向量中可能可能更快,然后将上下文返回为subvec s。

然而,完成后,组织生成上下文的一种简单方法是让context-fn返回与单词序列对应的上下文序列。为sliding-windows执行此操作的函数是

(defn sliding-windows [w s]
  (let [v (vec s), n (count v)
        window (fn [i] (lazy-cat (subvec v (max (- i w) 0) i)
                                 (subvec v (inc i) (min (inc (+ i w)) n))))]
    (map window (range n))))

我们现在可以根据新类型compute-space定义contexts-fn函数,如下所示:

(defn compute-space [docs contexts-fn target?]
  (letfn [(stuff [s] (->> (map vector s (contexts-fn s))
                          (filter (comp target? first))))]
    (reduce
     (fn [a [k m]] (assoc a k (merge-with + (a k) (frequencies m))))
     {}
     (mapcat stuff docs))))

代码在stuff上转动:

  • 我们将stuff定义为[target context-sequence]对的序列。
  • 然后我们将每对合并到聚合中,为每个目标事件添加相应的邻居计数。

<强>结果

此算法比问题中的算法快约500倍:问题中的代码在一天半内完成,这应该在大约一分钟内执行。

鉴于

  • 一个10万字的词汇,
  • 一句10万字,
  • 10,000个目标

此代码在100毫秒内构造上下文映射。

对于句子十分之一 - 10,000个单词 - 问题中的代码需要5秒。

这是使用(长)整数而不是字符串作为“单词”。因此,使用哈希值组合字符串的工作将在一定程度上淡化改进。

注意

我把这个答案的原始版本缩小了,因为

  • 代码中有转录错误;
  • 表现声称不准确。

通过Criterium进行的精确测试 - 使用瞬态贴图的版本变得稍慢,因此省略了。