在 Clojure 中处理大文件时如何利用内存/性能

问题描述

在处理大量时间序列数据时如何利用内存/性能

大小:~3.2G

行数:约 5400 万

数据集的前几行

{: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")))

为了获得最佳性能,我应该尽可能多地将数据检索到内存中。但是,我的笔记本只有 16GB,当我将更多数据检索到内存中时,cpu/内存利用率几乎达到 95%。

  1. 我能否对 Clojure 中的大型数据集进行更好的内存管理?
  2. 我可以保留内存缓冲区来存储数据集吗?
  3. 因为这是小内存环境下的时间序列数据。处理第一批数据时,可以通过line-seq检索下一批数据。
  4. 请建议使用什么数据结构来实现此功能

请随时发表评论

谢谢

解决方法

因为数据集仅包含 54000000 行,如果将数据打包在内存中,您可以将该数据集放入内存中。假设这是你想要做的,例如为了方便随机访问,这里提供一种方法。

您无法将其放入内存的原因可能是用于表示从文件中读取的每条记录的所有对象的开销。但是,如果您将这些值展平到例如一个字节缓冲区中,那么存储这些值所需的空间量就不是那么大了。您可以将时间戳简单地表示为每个数字一个字节,并使用一些定点表示法来表示数量。这是一个快速而肮脏的解决方案。

(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 GB,这将适合内存(尽管您可能需要调整 JVM 的某些标志......)。

如果您需要加载比这更大的文件,您可以考虑将数据放入 memory-mapped file 而不是内存中的字节缓冲区。

,

使用 deftype 创建一个长 ts 和 doubles 的类型,用于投标询价。如果您将行字符串解析为这种类型的实例,您会发现 5400 万行数据集应该很容易放入内存中。 24 个字节的数据,加上 8 个字节的对象头,加上数组中的 ~8 个字节的引用构成 40 个字节/记录。大约 2G 堆。

更多奇特的解决方案(用于列存储的原始数组,或用于访问压缩字节缓冲区的享元)是可能的,但对于您指定的问题参数来说是不需要的。

要遵循的示例代码,我手头只有手机。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...