Clojure:存储和编译大型派生数据结构

Dav*_*ams 5 tree serialization compilation clojure flat-file

我有一个大型数据结构,一棵树,占用大约2GB的内存.它包括叶子中的clojure集合,以及作为分支的refs.通过读取和解析大型平面文件并将行插入树中来构建树.然而,这需要大约30秒.有没有办法我可以构建一次树,将它发送到clj文件,然后将树编译到我的独立jar中,这样我就可以在树中查找值而无需重新读取大文本文件?我认为这将减少30秒的树构建,但这也将帮助我部署我的独立jar,而不需要文本文件来进行.

我的第一次摇摆失败了:

(def x (ref {:zebra (ref #{1 2 3 4})}))
#<Ref@6781a7dc: {:zebra #<Ref@709c4f85: #{1 2 3 4}>}>

(def y #<Ref@6781a7dc: {:zebra #<Ref@709c4f85: #{1 2 3 4}>}>)
RuntimeException Unreadable form  clojure.lang.Util.runtimeException (Util.java:219)
Run Code Online (Sandbox Code Playgroud)

Mic*_*zyk 8

由于对JVM施加的大小限制,可能无法在编译代码中嵌入这么大的数据.特别是,没有一种方法的长度可能超过64 KiB.以我在下面进一步描述的方式嵌入数据也需要在它将要存在的类文件中包含大量的东西; 看起来不是一个好主意.

假设你正在使用的数据结构,只读,你可以建,这一次,然后它发出一个.clj/ .edn(这对EDN的基础上,Clojure的文字符号的序列化格式),然后包括你的类路径作为在该文件"资源",因此它被包含在überjar(在resources/默认Leiningen设置;它当然,要想列入überjar除非排除:uberjar-exclusionsproject.clj),并在运行时资源在Clojure的读者全速阅读:

(ns foo.core
  (:require [clojure.java.io :as io]))

(defn get-the-huge-data-structure []
  (let [r   (io/resource "huge.edn")
        rdr (java.io.PushbackReader. (io/reader r))]
    (read r)))

;; if you then do something like this:

(def ds (get-the-huge-data-structure))

;; your app will load the data as soon as this namespace is required;
;; for your :main namespace, this means as soon as the app starts;
;; note that if you use AOT compilation, it'll also be loaded at
;; compile time
Run Code Online (Sandbox Code Playgroud)

您也可以不将其添加到überjar,而是在运行应用程序时将其添加到类路径中.这样你的überjar本身就不会是巨大的.

处理持久性Clojure数据以外的东西可以使用print-method(序列化时)和读取器标记(反序列化时)来完成.亚瑟已经证明了使用读卡器标签 使用print-method,你会做类似的事情

(defmethod print-method clojure.lang.Ref [x writer]
  (.write writer "#ref ")
  (print-method @x writer))

;; from the REPL, after doing the above:

user=> (pr-str {:foo (ref 1)})
"{:foo #ref 1}"
Run Code Online (Sandbox Code Playgroud)

当然,您只需要print-method在序列化时定义方法; 你反序列化代码可以不管它,但需要适当的数据读取器.


暂时忽略代码大小问题,因为我发现数据嵌入问题很有趣:

假设你的数据结构只包含本地的Clojure处理不可变的数据(Clojure的持久化集合,任意嵌套,加上原子项目,如数字,字符串(原子用于此目的),关键字,符号;无参考文献等),你的确可以包括它在你的代码中:

(defmacro embed [x]
  x)
Run Code Online (Sandbox Code Playgroud)

然后x,通过使用类文件中包含的常量和类的静态方法clojure.lang.RT(例如RT.vectorRT.map),生成的字节码将在不读取任何内容的情况下重新创建.

当然,这是如何编译文字的,因为上面的宏是一个noop.我们可以让事情变得更有趣:

(ns embed-test.core
  (:require [clojure.java.io :as io])
  (:gen-class))

(defmacro embed-resource [r]
  (let [r   (io/resource r)
        rdr (java.io.PushbackReader. (io/reader r))]
    (read r)))

(defn -main [& args]
  (println (embed-resource "foo.edn")))
Run Code Online (Sandbox Code Playgroud)

这将foo.edn在编译时读取并将结果嵌入到已编译的代码中(从包含适当的常量和代码的意义上来重构类文件中的数据).在运行时,不会执行进一步的读取.