2015-12-17 3 views
5

Я обрабатываю текстовые файлы размером 60 ГБ или больше. Файлы разделяются на секцию заголовка переменной длины и раздел данных. У меня есть три функции:Clojure - обрабатывать огромные файлы с низкой памятью

  • head? Предикат для различения заголовков строк из строк данных
  • process-header процесса один строка заголовка строка
  • process-data процесс один линия данных строк
  • Функции обработки асинхронно доступа и модифицирует in-memory

Я применил метод чтения файлов из другого потока SO, который должен построить ленивая последовательность строк. Идея состояла в том, чтобы обработать некоторые строки с помощью одной функции, затем один раз переключить функцию и продолжить обработку следующей функцией.

(defn lazy-file 
    [file-name] 
    (letfn [(helper [rdr] 
      (lazy-seq 
      (if-let [line (.readLine rdr)] 
       (cons line (helper rdr)) 
       (do (.close rdr) nil))))] 
    (try 
     (helper (clojure.java.io/reader file-name)) 
     (catch Exception e 
     (println "Exception while trying to open file" file-name))))) 

Я использую его с чем-то вроде

(let [lfile (lazy-file "my-file.txt")] 
    (doseq [line lfile :while head?] 
    (process-header line)) 
    (doseq [line (drop-while head? lfile)] 
    (process-data line))) 

Хотя это работает, это довольно неэффективно по нескольким причинам:

  • Вместо того, чтобы просто вызвать process-head, пока я не достигну данных и затем, продолжая с process-data, я должен фильтровать строки заголовка и обрабатывать их, а затем перезапускать парсинг всего файла и отбрасывать все строки заголовка для обработки данных. Это полная противоположность тому, что должно было делать lazy-file.
  • Наблюдение за потреблением памяти показывает мне, что программа, хотя и кажущаяся ленивой, позволяет использовать столько оперативной памяти, сколько потребуется для хранения файла в памяти.

Итак, что является более эффективным идиоматическим способом работы с моей базой данных?

Одна идея может использовать мультиметод для обработки заголовка и данных, зависящих от значения предиката head?, но я полагаю, что это имело бы какое-то серьезное влияние на скорость, тем более что есть только одно место, где результат предиката изменяется от всегда true всегда всегда false. Я еще не оценил это.

Было бы лучше использовать другой способ построения строки-seq и проанализировать ее с помощью iterate? Это все равно оставит меня нужным использовать: while и: drop-while, я думаю.

В моих исследованиях несколько раз упоминался доступ к файлам NIO, что должно улучшить использование памяти. Я еще не мог узнать, как использовать это в идиоматическом ключе в clojure.

Возможно, у меня все еще плохое понимание общей идеи, как следует обрабатывать файл?

Как всегда, любая помощь, идеи или указатели на tats приветствуются.

ответ

0

Есть несколько вещей, чтобы рассмотреть здесь:

  1. Использование памяти

    Есть сообщения, что Leiningen может добавить материал, что приводит к поддержанию ссылки на голове, хотя doseq конкретно не удержать глава последовательности обработки, ср. this SO question. Попробуйте проверить свою претензию «используйте столько оперативной памяти, сколько потребуется для хранения файла в памяти», не используя lein repl.

  2. разборе строк

    Вместо того, чтобы использовать две петли с doseq, вы также можете использовать loop/recur подход. Что вы ожидаете разбора будет второй аргумент, как это (непроверенные):

    (loop [lfile (lazy-file "my-file.txt") 
          parse-header true] 
         (let [line (first lfile)] 
          (if [and parse-header (head? line)] 
           (do (process-header line) 
            (recur (rest lfile) true)) 
           (do (process-data line) 
            (recur (rest lfile) false))))) 
    

    Существует еще один вариант здесь, который будет включать ваши функции обработки в вашу функцию чтения файла. Таким образом, вместо того, чтобы просто ввести новую строку и вернуть ее, вы могли бы просто обработать ее сразу - как правило, вы могли бы передать функцию обработки в качестве аргумента вместо жесткого кодирования.

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

+0

Спасибо за ваш ответ. Вчера я написал несколько тестов для проведения бенчмаркинга. Оказалось, что ** A) ** Это не само чтение, которое потребляет столько памяти, кажется, это база данных (кстати, мои требования к потреблению памяти связаны с запуском скомпилированного приложения) ** B) * * '' '' lazy-file''' и '' 'line-seq''' выполняют примерно равные, учитывая скорость и использование памяти ** C) ** Удивительно, что многоточечные методы и подход с циклом-повторами потребуют около 150% времени, необходимого для открытия файла дважды и использования while/drop-while – waechtertroll

+0

Мне нравится ваш способ рекурсии при чтении файла. Следующая идея, которую я попробую, заключается в том, что у меня будет проверка заголовка-парсера, если следующая строка - это строка данных (стиль итератора), и если да, то батут прочь к парсеру данных. Если-else на каждой строке очень медленно, но файлы хорошо определены в несколько сотен строк заголовка и сотни миллионов строк данных, а чтение головы занимает менее половины секунды. Я еще не уверен, как объединить батут и итератор ... – waechtertroll

2

Вы должны использовать стандартные библиотечные функции.

line-seq, with-open и doseq будут легко выполнять эту работу.

Что-то в строке:

(with-open [rdr (clojure.java.io/reader file-path)] 
    (doseq [line (line-seq rdr)] 
    (if (head? line) 
     (process-header line) 
     (process-data line)))) 
+0

Спасибо за ваше предложение. Метод '' lazy-file''', который я использую, был реализован, когда я начал изучать clojure, убирался в модуле io и использовал оттуда. Чистый эффект от этого действительно тот же, что и при использовании '' 'line-seq'''. – waechtertroll

+0

Другая вспомогательная информация, подход if-else на линию оказался значительно медленнее (коэффициент 1,5), чем тот, который я принимал. Значительно потому, что время выполнения здесь измеряется в часах ;-) – waechtertroll

+0

Я понимаю ваш аргумент о 'lazy-file', но работа с открытием и закрытием файла усложняет эту функцию для модульного тестирования. – kawas44

Смежные вопросы