Clojure lazy-seq оптимизация производительности

Я новичок в Clojure, и у меня есть код, который я пытаюсь оптимизировать. Я хочу вычислить количество совпадений. Основная функция - это compute-space, а вывод - это вложенная карта типа

{"w1" {"w11" 10, "w12" 31, ...}
 "w2" {"w21" 14, "w22" 1,  ...}
 ... 
 }

это означает, что "w1" совпадает с "w11" 10 раз и т. д.

Требуется набор документов (предложений) и набор целевых слов, он выполняет итерацию по обоим и, наконец, применяет context-fn, например, скользящее окно, для извлечения контекста- слова. Более конкретно, я закрываю скользящее окно

(compute-space docs (fn [target doc] (sliding-window target doc 5)) targets)

Я тестировал его примерно с 50 миллионами слов (~ 3 миллиона предложений) и примерно. 20000 целей. На создание этой версии уйдет больше суток. Я также написал параллельную функцию 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 довольно сильно доминируют в процессе (см. Рисунок). Это, безусловно, связано с тем фактом, что я использую этот двойной цикл duplicq (предыдущие эксперименты показали, что этот способ был быстрее, чем использование map, reduce и т.п., хотя я использовал time для тестирования функций). Я очень благодарен за любую информацию, которую вы могли бы мне предоставить, и за предложения по рефакторингу кода и ускорению его работы. Думаю, здесь могут помочь редукторы, но я не уверен, как и / или почему.

профиль памяти jvisualvm

ТЕХНИЧЕСКИЕ ХАРАКТЕРИСТИКИ

MacPro 2010 2,4 ГГц Intel Core 2 Duo 4 ГБ ОЗУ

Clojure 1.6.0

Java 1.7.0_51 Java HotSpot (TM) 64-разрядная серверная виртуальная машина

Тестовые данные

GithubGist со всем кодом


person emanjavacas    schedule 07.04.2015    source источник
comment
извиняюсь! Я сделал рефакторинг   -  person emanjavacas    schedule 07.04.2015
comment
Не связано с оптимизацией, но (partial inc) по сути то же самое, что inc.   -  person Shannon Severance    schedule 07.04.2015
comment
sliding-window принимает три аргумента, но только два передаются в context-fn.   -  person Shannon Severance    schedule 07.04.2015
comment
@ShannonSeverance, спасибо за наблюдение. В случае context-fn это фактически закрытие, переданное в compute-space. (fn [цель отправлена] (цель скользящего окна отправлена ​​5)), например. Я отредактирую вопрос, чтобы прояснить это   -  person emanjavacas    schedule 07.04.2015


Ответы (2)


Данные испытаний были:

  • Ленивая последовательность из 42 строк (целей)
  • Ленивая последовательность из 105 040 ленивых наборов. (Документы)
  • Каждая ленивая последовательность в Documents представляла собой ленивую последовательность строк. Общее количество строк, содержащихся в документах, составило 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 г., четырехъядерный процессор Intel Xeon с тактовой частотой 2,66 ГГц, 48 ГБ ОЗУ.
  • Clojure 1.6.0.
  • Java 1.8.0_40 Java HotSpot (TM) 64-разрядная серверная виртуальная машина.

TBD

Дальнейшие оптимизации.

Опишите данные теста.

person Shannon Severance    schedule 08.04.2015
comment
Прежде всего, спасибо за ваш код и помощь! Попробовав его на своем компьютере, я немного озадачен, потому что получил противоречивые результаты. Я добавил свои спецификации в github gist со всем задействованным кодом (функция ввода-вывода и т. Д.), Результатами критериев и ссылкой на тестовый файл, который я использую. Любые идеи относительно того, почему это расхождение? - person emanjavacas; 08.04.2015
comment
@emanjavacas Интересно. Но не знаю, успею ли я его посмотреть. - person Shannon Severance; 08.04.2015
comment
Поскольку я не получил ответа от OP, могу ли я попросить вас проверить мою работу? - person Thumbnail; 17.04.2015
comment
@Thumbnail, у вас там есть несколько хороших идей, которые подкреплены вашим собственным временем. У меня нет времени для проверки или критики вашей работы по этой проблеме. Извините. - person Shannon Severance; 18.04.2015
comment
Я дошел до использования Criterium. Одновременное тестирование всех целей действительно дает огромное улучшение, но переход к переходным процессам на самом деле замедляет работу! По-прежнему ни слова от ОП. - person Thumbnail; 21.04.2015

Анализ алгоритма compute-space в вопросе

Стоимость сканирования предложений - поиска целей -

  • пропорционально общему количеству слов
  • пропорционально количеству целей, но
  • более или менее не зависит от количества предложений, на которые разделены слова.

Стоимость борьбы с целями

  • пропорциональна скорости попадания в цель,
  • пропорционально количеству отдельных слов в своем контексте.

Значительное улучшение

context-fn просматривает предложение в поисках цели. Если есть десять тысяч целей, он сканирует предложение десять тысяч раз.

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

Возможное улучшение

Функция sliding-windows генерирует контексты, передавая каждое слово из рук в руки - от todo до seen. Вероятно быстрее вылить слова в вектор, а затем вернуть контексты как subvecs.

Как бы то ни было, простой способ организовать создание контекстов - заставить 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 000 слов,
  • одно предложение из 100 000 слов и
  • 10000 целей

этот код строит контекстную карту за 100 мсек.

Для предложения в десять раз меньше - 10 000 слов - код в вопросе занимает 5 секунд.

Здесь в качестве «слов» используются (длинные) целые числа, а не строки. Таким образом, сборка строк с их хеш-значениями несколько ослабит улучшение.

Примечание

Я убрал исходную версию этого ответа, потому что

  • в коде была ошибка транскрипции;
  • Заявления о производительности не были точными.

При точных тестах, проведенных Criterium, версия, использующая карты переходных процессов, оказалась немного медленнее, поэтому она была опущена.

person Thumbnail    schedule 10.04.2015
comment
извините, я не был уведомлен о вашем ответе. Спасибо за Ваш ответ. К сожалению, у меня не будет времени проверить ваши предложения в ближайшее время, но они звучат интересно. Еще одна вещь, которую я хотел бы попробовать, - это перейти в структуру редукторов. - person emanjavacas; 08.05.2015
comment
@emanjavacas Переход к фреймворку редукторов / преобразователей приведет к постоянному улучшению коэффициента - я бы предположил, что от 2 до 5, хотя может быть и больше. Приведенный выше ответ дает улучшение, пропорциональное количеству целей: в вашем случае примерно в 10К. - person Thumbnail; 08.05.2015