为什么pmap | reducers / map没有使用所有cpu核心?

时间:2016-05-14 17:10:54

标签: clojure reducers pmap cheshire

我正在尝试解析一百万行的文件,每行都是一个json字符串,其中包含一些关于书籍的信息(作者,内容等)。我正在使用iota加载文件,因为如果我尝试使用OutOfMemoryError,我的程序会抛出slurp。我也使用cheshire来解析字符串。该程序只需加载一个文件并计算所有书籍中的所有单词。

我的第一次尝试包括pmap做繁重的工作,我认为这基本上会利用我所有的cpu核心。

(ns multicore-parsing.core
  (:require [cheshire.core :as json]
            [iota :as io]
            [clojure.string :as string]
            [clojure.core.reducers :as r]))


(defn words-pmap
  [filename]
  (letfn [(parse-with-keywords [str]
            (json/parse-string str true))
          (words [book]
            (string/split (:contents book) #"\s+"))]
    (->>
     (io/vec filename)
     (pmap parse-with-keywords)
     (pmap words)
     (r/reduce #(apply conj %1 %2) #{})
     (count))))

虽然它似乎确实使用了所有核心,但每个核心很少使用超过50%的容量,我的猜测是它与pmap的批量大小有关,所以我偶然发现了relatively old question的一些注释引用clojure.core.reducers库。

我决定使用reducers/map重写函数:

(defn words-reducers
  [filename]
  (letfn [(parse-with-keywords [str]
            (json/parse-string str true))
          (words [book]
            (string/split (:contents book) #"\s+"))]
  (->>
   (io/vec filename)
   (r/map parse-with-keywords)
   (r/map words)
   (r/reduce #(apply conj %1 %2) #{})
   (count))))

但是cpu的使用情况更糟糕,与之前的实现相比,它甚至需要更长的时间才能完成:

multicore-parsing.core=> (time (words-pmap "./dummy_data.txt"))
"Elapsed time: 20899.088919 msecs"
546
multicore-parsing.core=> (time (words-reducers "./dummy_data.txt"))
"Elapsed time: 28790.976455 msecs"
546

我做错了什么?在解析大文件时,mmap loading + reducers是正确的方法吗?

编辑:this是我正在使用的文件。

EDIT2:以下是iota/seq代替iota/vec的时间安排:

multicore-parsing.core=> (time (words-reducers "./dummy_data.txt"))
"Elapsed time: 160981.224565 msecs"
546
multicore-parsing.core=> (time (words-pmap "./dummy_data.txt"))
"Elapsed time: 160296.482722 msecs"
546

1 个答案:

答案 0 :(得分:3)

我不相信减速器对你来说是正确的解决方案,因为它们根本不能很好地处理惰性序列(减速器会以懒惰序列给出正确的结果,但不能很好地并行化)。

您可能需要查看本书sample code中的Seven Concurrency Models in Seven Weeks(免责声明:我是作者),它解决了类似的问题(计算每个单词出现在维基百科上的次数)

给定维基百科页面列表,此函数按顺序计算单词(get-words返回页面中的单词序列):

(defn count-words-sequential [pages]
  (frequencies (mapcat get-words pages)))

这是使用pmap的并行版本,运行速度更快,但速度只提高了1.5倍:

(defn count-words-parallel [pages]
  (reduce (partial merge-with +)
    (pmap #(frequencies (get-words %)) pages)))

它只能快1.5倍的原因是因为reduce成为瓶颈 - 它为每个页面调用(partial merge-with +)一次。合并100页的批次可将性能提高到4核机器上的3.2倍左右:

(defn count-words [pages]
  (reduce (partial merge-with +)
    (pmap count-words-sequential (partition-all 100 pages))))