5

我有一个大型数据结构,一棵树,它占用了大约 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)
4

3 回答 3

8

由于 JVM 的大小限制,在编译代码中嵌入这么大的数据可能是不可能的。特别是,任何一种方法的长度都不能超过 64 KiB。以我在下面进一步描述的方式嵌入数据也需要在它将要存在的类文件中包含大量内容;似乎不是一个好主意。

鉴于您使用的是只读数据结构,您可以构建它一次,然后将其发送到.clj/ .edn(用于edn,基于 Clojure 文字符号的序列化格式),然后将该文件包含在您的类路径中作为“资源”,以便它包含在 überjar 中(在resources/默认 Leiningen 设置中;然后它将被包含在 überjar 中,除非被:uberjar-exclusionsin排除project.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

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

可以使用print-method(序列化时)和阅读器标签(反序列化时)来处理持久 Clojure 数据以外的东西。Arthur 已经演示了使用阅读器标签;使用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}"

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


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

假设您的数据结构仅包含由 Clojure 本地处理的不可变数据(Clojure 持久集合,任意嵌套,加上原子项目,如数字、字符串(用于此目的的原子)、关键字、符号;没有 Refs 等),您确实可以包含它在您的代码中:

(defmacro embed [x]
  x)

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

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

于 2013-05-09T22:56:36.327 回答
3

如果您可以将树构造为​​单个值而不是引用许多值的树,那么您将能够打印并读取树。因为 refs 不可读,如果不进行自己的解析,您将无法将整个树视为可读的东西。

可能值得考虑使用可扩展阅读器通过将其设为类型来为您的树添加打印和阅读功能。

这是使用数据读取器从字符串生成对集合和映射的引用的最小示例:

首先为每个 EDN 标签/类型的内容定义处理程序

user> (defn parse-map-ref [m] (ref (apply hash-map m)))
#'user/parse-map-ref
user> (defn parse-set-ref [s] (ref (set s)))
#'user/parse-set-ref

然后绑定地图数据阅读器以将处理程序与文本标签相关联:

(def y-as-string 
   "#user/map-ref [:zebra #user/set-ref [1 2 3 4]]")

user> (def y (binding [*data-readers* {'user/set-ref user/parse-set-ref
                                       'user/map-ref user/parse-map-ref}]
              (read-string y-as-string)))

user> y
#<Ref@6d130699: {:zebra #<Ref@7c165ec0: #{1 2 3 4}>}> 

这也适用于嵌套更深的树:

(def z-as-string 
  "#user/map-ref [:zebra #user/set-ref [1 2 3 4] 
                  :ox #user/map-ref [:amimal #user/set-ref [42]]]")

user> (def z (binding [*data-readers* {'user/set-ref user/parse-set-ref
                                       'user/map-ref user/parse-map-ref}]
               (read-string z-as-string)))
#'user/z
user> z
#<Ref@2430c1a0: {:ox #<Ref@7cf801ef: {:amimal #<Ref@7e473201: #{42}>}>, 
                 :zebra #<Ref@7424206b: #{1 2 3 4}>}> 

从树中生成字符串可以通过扩展 print-method 多方法来完成,尽管如果您使用 deftype 为 ref-map 和 ref-set 定义类型会容易得多,这样打印机就可以知道哪个 ref 应该生成哪些字符串。

如果通常将它们作为字符串读取太慢,则可以使用更快的二进制序列化库,例如协议缓冲区。

于 2013-05-09T18:52:50.543 回答
3

这种结构是不变的吗?如果没有,请考虑使用 Java 序列化来持久化结构。反序列化将比每次重建要快得多。

于 2013-05-09T20:02:14.117 回答