Почему pmap|reducers/map не использует все ядра процессора?

Я пытаюсь разобрать файл с миллионом строк, каждая строка представляет собой строку json с некоторой информацией о книге (автор, содержание и т. д.). Я использую iota для загрузки файла, так как моя программа выдает OutOfMemoryError, если я пытаюсь использовать slurp. Я также использую cheshire для разбора строк. Программа просто загружает файл и считает все слова во всех книгах.

Моя первая попытка включала pmap для выполнения тяжелой работы, я полагал, что это по существу задействует все ядра моего процессора.

(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, и поэтому я наткнулся на относительно старый вопрос, где в некоторых комментариях упоминается библиотека 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))))

Но загрузка процессора хуже, и для завершения даже требуется больше времени по сравнению с предыдущей реализацией:

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 + редукторы правильным подходом при разборе большого файла?

РЕДАКТИРОВАТЬ: это файл, который я использую.

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

person eugecm    schedule 14.05.2016    source источник
comment
Похоже, что io/vec сканирует весь файл, чтобы построить индекс того, где находятся строки. Получаете ли вы другие результаты, если попробуете io/seq?   -  person Nathan Davis    schedule 14.05.2016
comment
@NathanDavis Я только что попробовал, времена еще хуже. позвольте мне обновить вопрос   -  person eugecm    schedule 14.05.2016
comment
Это выступление Леона Барретта, автора Claypoole может содержать соответствующую информацию. Он подробно объясняет pmap, в том числе почему он часто не загружает ЦП, и немного о том, почему включение одного pmap в другое может привести к удивительным результатам. Кроме того, если вы в основном ищете способ насытить свой процессор, Claypoole может быть именно тем, что вам нужно.   -  person Ben Kovitz    schedule 15.05.2016
comment
Не нагружая ЦП: похоже, что он связан с вводом-выводом. Возможно, было бы полезно использовать line-seq, который лениво читает строки. Кроме того, не вызывайте pmap дважды подряд. Лучше использовать (pmap (comp words parse-with-keywords)). Постарайтесь упаковать как можно больше обработки в один вызов pmap, потому что создание нескольких потоков каждый раз при его вызове сопряжено с большими накладными расходами. Если обработка, выполняемая с помощью одного вызова pmap, слишком мала, его не стоит использовать.   -  person Mars    schedule 15.05.2016
comment
Обычно лучше всего замерять время с помощью библиотеки Criterium, хотя в вашем случае это может не иметь значения.   -  person Mars    schedule 15.05.2016
comment
Исправление: я не верю, что line-seq поможет, если он связан с вводом-выводом - я не думал - но, возможно, стоит попробовать в любом случае.   -  person Mars    schedule 15.05.2016
comment
Интересный. После некоторого профилирования я заметил, что iota на самом деле требуется много времени, чтобы разделить куски. Использование clojure.java.io/reader с line-seq сократило время почти вдвое (с pmap). Я думаю, нет необходимости загружать весь файл в память, если я могу обрабатывать его построчно.   -  person eugecm    schedule 15.05.2016


Ответы (1)


Я не верю, что редукторы будут правильным решением для вас, так как они совсем не справляются с ленивыми последовательностями (редюсер даст правильные результаты с ленивой последовательностью, но плохо распараллелит).

Вы можете взглянуть на этот пример кода из книги Семь моделей параллелизма за семь недель (отказ от ответственности: я являюсь автором), который решает аналогичную проблему (подсчет количества раз, которое каждое слово появляется в Википедии).

Учитывая список страниц Википедии, эта функция последовательно подсчитывает слова (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 страниц повышает производительность примерно в 3,2 раза на 4-ядерной машине:

(defn count-words [pages]
  (reduce (partial merge-with +)
    (pmap count-words-sequential (partition-all 100 pages))))
person Paul Butcher    schedule 16.05.2016
comment
была ли pages ленивой последовательностью? или он был предварительно загружен со всеми страницами? - person eugecm; 18.05.2016
comment
pages ленив, да. - person Paul Butcher; 19.05.2016
comment
Вы можете увидеть источник загрузки страниц здесь: media.pragprog.com/titles/pb7con/code/FunctionalProgramming/ и для полноты реализации get-words здесь: media.pragprog.com/titles/pb7con/code/FunctionalProgramming/ - person Paul Butcher; 19.05.2016