Как использовать память/производительность при обработке большого файла в Clojure

Как использовать память/производительность при обработке большого набора данных временных рядов?

Размер: ~ 3,2 г

Строки: ~ 54 миллиона

Первые несколько строк набора данных

{:ts 20200601040025269 :bid 107.526000 :ask 107.529000}
{:ts 20200601040025370 :bid 107.525000 :ask 107.529000}
{:ts 20200601040026421 :bid 107.525000 :ask 107.528000}
{:ts 20200601040026724 :bid 107.524000 :ask 107.528000}
{:ts 20200601040027424 :bid 107.524000 :ask 107.528000}
{:ts 20200601040033535 :bid 107.524000 :ask 107.527000}
{:ts 20200601040034230 :bid 107.523000 :ask 107.526000}

Вспомогательные функции

(defn lines [n filename]
  (with-open [rdr (io/reader filename)]
    (doall (take n (line-seq rdr)))))

(def dataset (into [] (lines 2000 "./data/rawdata.map")))

Для лучшей производительности я должен как можно больше извлекать данные в память. Однако в моем ноутбуке всего 16 ГБ, когда я загружаю больше данных в память, ЦП / память используются почти на 95%.

  1. Могу ли я лучше управлять памятью с большим набором данных в Clojure?
  2. Могу ли я зарезервировать буфер памяти для хранения набора данных?
  3. Потому что это данные временных рядов в небольшой среде памяти. Когда первый пакет данных обработан, следующий пакет может быть получен с помощью line-seq.
  4. Подскажите, пожалуйста, какая структура данных используется для реализации этой функции?

Пожалуйста, не стесняйтесь комментировать.

Спасибо


person madeinQuant    schedule 04.05.2021    source источник
comment
Рассматривали ли вы возможность использования библиотеки обработки данных, такой как tech.ml.dataset?   -  person Steffan Westcott    schedule 04.05.2021
comment
@SteffanWestcott, tech.ml.dataset великолепен и имеет множество функций. В моем случае мы реализуем небольшой проект и хотим поддерживать автономную кодовую базу. Спасибо за ваш комментарий.   -  person madeinQuant    schedule 04.05.2021
comment
Очевидно, вам нужно будет каким-то образом работать с порциями данных. Вы уже используете ленивую последовательность (через line-seq). Кроме того, вам нужно будет добавить дополнительные сведения о предполагаемой обработке данных.   -  person Alan Thompson    schedule 04.05.2021
comment
@AlanThompson, спасибо за ваш комментарий. Мы хотим минимизировать дисковый ввод-вывод, чтобы максимизировать производительность.   -  person madeinQuant    schedule 04.05.2021
comment
Этот вопрос является как чрезмерно, так и недостаточно определенным. Никто не может посоветовать вам, как читать файл, не зная, как вы планируете использовать полученные данные. Вам нужен произвольный доступ? Будете ли вы получать доступ к каждому элементу только один раз? И так далее. Совет Алана Томпсона — очевидный подход. Вы отказываетесь от него, чтобы свести к минимуму дисковый ввод-вывод, но он выполняет не больше операций ввода-вывода, чем любой другой подход. Такие ограничения чрезмерно конкретизируют вопрос.   -  person amalloy    schedule 04.05.2021


Ответы (2)


Поскольку набор данных состоит всего из 54 000 000 строк, вы можете разместить этот набор данных в памяти, если соберете данные вместе в памяти. Предполагая, что это то, что вы хотите сделать, например. ради удобства произвольного доступа вот один из подходов.

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

(def fixed-pt-factor 1000)
(def record-size (+ 17 4 4))
(def max-count 54000000)

(defn put-amount [dst amount]
  (let [x (* fixed-pt-factor amount)]
    (.putInt dst (int x))))


(defn push-record [dst m]
  ;; Timestamp (convert to string and push char by char)
  (doseq [c (str (:ts m))]
    (.put dst (byte c)))
  (put-amount dst (:bid m))
  (put-amount dst (:ask m))
  dst)

(defn get-amount [src pos]
  (/ (BigDecimal. (.getInt src pos))
     fixed-pt-factor))

(defn record-count [dataset]
  (quot (.position dataset) record-size))

(defn nth-record [dataset n]
  (let [offset (* n record-size)]
    {:ts (edn/read-string (apply str (map #(->> % (+ offset) (.get dataset) char) (range 17))))
     :bid (get-amount dataset (+ offset 17))
     :ask (get-amount dataset (+ offset 17 4))}))

(defn load-dataset [filename]
  (let [result (ByteBuffer/allocate (* record-size max-count))]
    (with-open [rdr (io/reader filename)]
      (transduce (map edn/read-string) (completing push-record) result (line-seq rdr)))
    result))

Затем вы можете использовать load-dataset для загрузки набора данных, record-count для получения количества записей и nth-record для получения n-й записи:

(def dataset (load-dataset filename))

(record-count dataset)
;; => 7

(nth-record dataset 2)
;; => {:ts 20200601040026421, :bid 107.525M, :ask 107.528M}

Как именно вы решите представлять значения в байтовом буфере, зависит от вас, я специально не оптимизировал. Для загруженного набора данных в этом примере потребуется всего около 54000000 * 25 байт = 1,35 ГБ, что уместится в памяти (хотя вам, возможно, придется настроить какой-то флаг JVM...).

Если вам нужно загрузить файлы большего размера, вы можете рассмотреть возможность помещения данных в файл с отображением памяти вместо байтового буфера в памяти.

person Rulle    schedule 05.05.2021
comment
Спасибо за вашу помощь, я применю ваше предложение в моем проекте. Помимо этого, не могли бы вы предложить некоторые материалы для чтения, посвященные управлению большими наборами данных в JVM? - person madeinQuant; 06.05.2021
comment
Я нашел пример отображения памяти в Clojure. github.com/ clojure-поваренная книга/clojure-поваренная книга/blob/master/. Спасибо за ваше предложение. - person madeinQuant; 06.05.2021
comment
Отображенный в память файл представляет собой большой массив байтов в памяти, а не структуры данных, закодированные в файле. Как мы можем связать файл с отображением памяти со структурой данных clojure, например, с атомом - person madeinQuant; 25.05.2021

Используйте deftype, чтобы создать тип с длинным ts и удвоением для bid ask. Если вы разберете свои строковые строки на экземпляры этого типа, вы обнаружите, что набор данных из 54 миллионов строк должен легко помещаться в памяти. 24 байта данных, плюс 8 байтов заголовка объекта, плюс ~8 байтов ссылки в массиве составляют 40 байт/запись. Около 2G куча.

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

Пример кода для подражания, у меня есть только мой телефон.

person pete23    schedule 04.05.2021