我是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 在这里可能有所帮助,但我不确定如何和/或为什么。
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
答案 0 :(得分:4)
测试数据是:
比你的工作负荷小一点。 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
<强>规格强>
<强> TBD 强>
进一步优化。
描述测试数据。
答案 1 :(得分:1)
分析问题中的compute-space
算法
扫描句子的费用 - 寻找目标 -
处理目标的成本
重大改进
context-fn
扫描寻找目标的句子。 如果有一万个目标,它会扫描句子一万次。
更好地扫描句子一次,寻找所有目标。如果我们将目标保持为(哈希)集合,我们可以测试一个单词是否是一个或多或少恒定时间的目标,无论有多少目标。
可能的改进
sliding-windows
函数通过将每个单词从一个接一个地传递到另一个单词来生成上下文 - 从todo
到seen
。将单词倒入向量中可能可能更快,然后将上下文返回为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倍:问题中的代码在一天半内完成,这应该在大约一分钟内执行。
鉴于
此代码在100毫秒内构造上下文映射。
对于句子十分之一 - 10,000个单词 - 问题中的代码需要5秒。
这是使用(长)整数而不是字符串作为“单词”。因此,使用哈希值组合字符串的工作将在一定程度上淡化改进。
注意强>
我把这个答案的原始版本缩小了,因为
通过Criterium进行的精确测试 - 使用瞬态贴图的版本变得稍慢,因此省略了。