Нужно оптимизировать мой код Clojure, который занимает слишком много времени

У меня есть файл журнала размером 1,6 ГБ, содержащий 2 миллиона записей. Я читаю содержимое журнала в канал, выполняю некоторые преобразования и записываю содержимое обратно в другой канал.

Наконец, я записываю содержимое второго канала в файл.

Мой код работает нормально, и результаты соответствуют ожиданиям. Однако вся операция занимает ~45 секунд, что слишком долго.

Мне нужно сократить время.

(def reader-channel (delay (let [temp (chan)]
                         (go
                           (with-open [reader (clojure.java.io/reader "My_Big_Log")]
                             (doseq [ln (line-seq reader)]
                               (>! temp ln)))
                           (close! temp))
                         temp)))



(def writer-channel (chan))

(defn make-collection [] (loop [my-coll []] (let [item (<!! @reader-channel)]
  (if (nil? item)
    my-coll
    (do (let [temp (re-find #"[a-z]+\.[a-z]+\.[a-z]+" item)]
          (recur (conj my-coll temp))))))))

(def transformed-collection (delay (partition-by identity
                                             (remove nil? (sort (make-collection))))))

(defn transform [] (go-loop [counter 0]
(if (>= counter (count @transformed-collection))
  (do (close! writer-channel)
      (println "Goodbye"))
  (do (let [item (str "Referrer " (+ counter 1) ": "
                      (first (nth @transformed-collection counter)))]
        (>! writer-channel item))
      (let [item (str "Number of entries associated with this referrer: "
                      (count (nth @transformed-collection counter)))]
        (>! writer-channel item))
    (recur (inc counter))))))

(defn write-to-file [] (with-open [wrtr (clojure.java.io/writer "Result.txt" :append true)]
(loop []
  (when-let [temp (<!! writer-channel)]
    (.write wrtr (str temp "\n"))
    (recur)))))

Прошу прощения за неправильный отступ и форматирование.


person Shrey    schedule 04.07.2019    source источник
comment
вы рассматривали возможность использования профилировщика? Я использую yourkit, но VisualVM бесплатен.   -  person pete23    schedule 04.07.2019


Ответы (3)


transform выполняет несколько чрезвычайно дорогостоящих операций каждый раз в цикле. count и nth в ленивой последовательности занимают O(n) времени. Вместо того, чтобы использовать любой из них, обработайте последовательность лениво с помощью first и next.

person amalloy    schedule 04.07.2019
comment
Я подозреваю, что количество ленивых последовательностей составляет только O (n) в первый раз. Это, конечно, заставит реализовать весь сиквел, а не полениться. - person pete23; 04.07.2019
comment
Нет, каждый раз медленно. - person amalloy; 05.07.2019
comment
Я узнаю что-то новое каждый день. Спасибо - пошел и посмотрел LazySeq.java. Не ожидал этого. - person pete23; 05.07.2019

Я не люблю код-гольф, но кажется, что это довольно просто уменьшит. Мы хотим подсчитать частоту перехода, поэтому давайте просто сделаем это:

  (with-open [reader (clojure.java.io/reader "My_Big_Log")]
    (frequencies
     (map #(re-find #"[a-z]+\.[a-z]+\.[a-z]+")
          (line-seq reader))))

Подсчет рефереров путем создания списка всех 2 миллионов из них, а затем его сортировка и разбиение на разделы означает, что вы носите с собой большое количество ненужных данных. Это делает это в пространственной сложности O (рефереры), а не O (строки), что в зависимости от ваших журналов может быть огромным сокращением.

Мне также непонятно, почему вы используете core.async. Это мало что добавит к этому простому подсчету и очень затруднит понимание того, что происходит в коде.

Наконец - просто профиль. Он покажет вам много интересного о вашем коде, о котором вы, возможно, не знали.

person pete23    schedule 04.07.2019
comment
Вашему уменьшению нужен аргумент {}. Но лучше бы не переделывать frequencies: (frequencies (map #(re-find ...) (line-seq reader))) - person amalloy; 05.07.2019

sort на 2 млн записей работает медленно. Плюс count и nth также дороги в ленивой последовательности. Вы можете избежать их (вместе со всеми промежуточными последовательностями) с преобразователем. На моем MBP 2M записей заняло ~ 5 секунд.

(defn transform [input-f output-f]
  (let [read-ch  (chan 1 (comp (map (partial re-find #"[a-z]+\.[a-z]+\.[a-z]+"))
                               ;; remove other lines
                               (remove nil?)
                               ;; transducer bag is like a set but with counter. e.g. {"a.b.c" 1  "c.d.e" 3}
                               (bag)
                               ;; make each map entry as a sequence element (["a.b.c" 1] ["c.d.e" 3])
                               cat
                               ;; generate output lines
                               (map-indexed (fn [i [x cnt]]
                                              [(str "Referrer " i ": " x)
                                               (str "Number of entries associated with this referrer: " cnt)]))
                               ;; flatten the output lines  (["l1" "l2"] ["l3" "l4"]) => ("l1" "l2" "l3" "l4")
                               cat))
        write-ch (chan)]

    ;; wire up read-ch to write-ch
    (pipe read-ch write-ch true)

    ;; spin up a thread to read all lines into read-ch
    (thread
      (with-open [reader (io/reader input-f)]
        (<!! (onto-chan read-ch (line-seq reader) true))))

    ;; write the counted lines to output
    (with-open [wtr (io/writer output-f)]
      (loop []
        (when-let [temp (<!! write-ch)]
          (.write wtr (str temp "\n"))
          (recur))))))

(time
 (transform "input.txt" "output.txt"))
;; => "Elapsed time: 5286.222668 msecs"

А вот «одноразовый» счетный мешок, который я использовал:

(defn bag []
  (fn [rf]
    (let [state (volatile! nil)]
      (fn
        ([] (rf))
        ([result] (if @state
                    (try
                      (rf result @state)
                      (finally
                        (vreset! state nil)))
                    (rf result)))
        ([result input]
         (vswap! state update input (fnil inc 0))
         result)))))

Вот пример вывода:

Referrer 0: h.i.j
Number of entries associated with this referrer: 399065
Referrer 1: k.l.m
Number of entries associated with this referrer: 400809
Referrer 2: a.b.c
Number of entries associated with this referrer: 400186
Referrer 3: c.d.e
Number of entries associated with this referrer: 399667
Referrer 4: m.n.o
Number of entries associated with this referrer: 400273
person rmcv    schedule 05.07.2019
comment
Привет, спасибо, что помогли мне, ваш подход определенно более элегантен, но на моей машине Thinkpad с i5 и 16 ГБ памяти для получения результата требуется ~ 40 секунд. - person Shrey; 06.07.2019
comment
@ последовательные запуски заняли разное время, первый занял 41 секунду, а второй - 46 секунд. imgur.com/61ef1aK. Это ссылка на скриншот. - person Shrey; 06.07.2019
comment
Пожалуйста, проверьте производительность дискового ввода-вывода. Еще одна вещь, которую стоит попробовать, — обновить jdk до 11.0.1 (не 11.0.2) и убедиться, что core.async является последней версией. В Windows с jdk 8 и Clojure 1.10 моя версия по сравнению с вашей версией составляет 2 с: 32 с. - person rmcv; 06.07.2019