Я только что закончил читать "Программирование параллелизма на JVM" Venkat Subramaniam, и в этой книге автор использует в качестве одного из своих примеров подсчет размеров файлов в дереве каталогов. Он показывает реализации без параллелизма, использования очередей, защелки и использования акторов scala. В моей системе все параллельные реализации (очереди, защелки и акторы scala) могут выполняться менее чем за 9 секунд при выполнении итерации по моему каталогу / usr (OSX 10.6.8, Core Duo 2 ГГц, Intel G1 ssd 160 ГБ).
Я изучаю Clojure и решил портировать версию актера Scala на Clojure с использованием агентов. К сожалению, у меня было в среднем 11-12 секунд, что значительно медленнее, чем у остальных. Потратив DAYS, потянув меня за волосы, я обнаружил, что виновным был следующий фрагмент кода (processFile - это функция, которую я отправляю агенту (-ам) обработки файлов:
(defn processFile
[fileProcessor collectorAgent ^String fileName]
(let [^File file-obj (File. ^String fileName)
fileTotals (transient {:files 0, :bytes 0})]
(cond
(.isDirectory file-obj)
(do
(doseq [^File dir (.listFiles file-obj) :when (.isDirectory dir)]
(send collectorAgent addFileToProcess (.getPath dir)))
(send collectorAgent tallyResult *agent*)
(reduce (fn [currentTotal newItem] (assoc! currentTotal :files (inc (:files currentTotal))
:bytes (+ (:bytes currentTotal) newItem)))
fileTotals
(map #(.length ^File %) (filter #(.isFile ^File %) (.listFiles file-obj))))
(persistent! fileTotals))
(.isFile file-obj) (do (send collectorAgent tallyResult *agent*) {:files 1, :bytes (.length file-obj)}))))
Вы заметите, что я пытался использовать подсказки типа и переходный процесс для повышения производительности, но все безрезультатно. Я заменил приведенный выше код следующим:
(defn processChildren
[children]
(loop [entries children files 0 bytes 0 dirs '()]
(let [^File child (first entries)]
(cond
(not (seq entries)) {:files files, :bytes bytes, :dirs dirs}
(.isFile child) (recur (rest entries) (inc files) (+ bytes (.length child)) dirs)
(.isDirectory child) (recur (rest entries) files bytes (conj dirs child))
:else (recur (rest entries) files bytes dirs)))))
(defn processFile
[fileProcessor collectorAgent ^String fileName]
(let [{files :files, bytes :bytes, dirs :dirs} (processChildren (.listFiles (File. fileName)))]
(doseq [^File dir dirs]
(send collectorAgent addFileToProcess (.getPath dir)))
(send collectorAgent tallyResult *agent*)
{:files files, :bytes bytes}))
Эта версия работает наравне, если не быстрее, чем версия Scala, и практически идентична алгоритму, используемому в версии Scala. Я просто предположил, что функциональный подход к алгоритму будет работать так же хорошо.
Итак ... этот длинный вопрос сводится к следующему: Почему вторая версия быстрее?
Моя гипотеза состоит в том, что, хотя первая версия, использующая map / filter / lower для содержимого каталога, является более "функциональной", чем довольно обязательная обработка каталога второй версией, она намного менее эффективна, поскольку содержимое каталога повторяется несколько раз. Поскольку ввод-вывод файловой системы медленный, страдает вся программа.
Предполагая, что я прав, разве не безопасно сказать, что любой рекурсивный алгоритм файловой системы должен предпочитать императивный подход к производительности?
Я абсолютный новичок в Clojure, поэтому не стесняйтесь копировать мой код в клочья, если я делаю что-то глупое или идиоматическое.