0

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

尺寸:~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. 请建议使用什么数据结构来实现此功能?

请随意发表评论。

谢谢

4

2 回答 2

1

由于数据集仅包含 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 的一些标志......)。

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

于 2021-05-05T20:26:17.800 回答
0

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

更奇特的解决方案(用于列存储的原始数组,或用于访问打包字节缓冲区的享元)是可能的,但对于您声明的问题参数是不必要的。

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

于 2021-05-04T22:25:36.503 回答