【问题标题】:Clojure: Store and Compile Large Derived Data StructureClojure:存储和编译大型派生数据结构
【发布时间】:2013-05-04 07:34:05
【问题描述】:

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

【问题讨论】:

  • 如果它是一个只读结构,那么我建议完全避免使用refs。它们会减慢访问速度,使序列化更加棘手,并使整体结构使用更多内存。不可变的持久数据结构是你的朋友……使用它们!

标签: serialization clojure compilation tree flat-file


【解决方案1】:

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

可能值得研究使用extensible reader 为您的树添加打印和读取功能,方法是使其成为一个类型。

这是一个使用 data-readers 从字符串生成对集合和映射的引用的最小示例:

首先为每个 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

然后绑定地图 data-readers 以将处理程序与文本标签相关联:

(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 多方法来完成,但如果您为 ref-map 和 ref-set 定义一个类型会更容易使用 deftype 以便打印机可以知道哪个 ref 应该生成哪个字符串。

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

【讨论】:

  • 真的,不可能吗?我的意思是在上面的示例中,如果您将#&lt;Ref@6781a7dc: 更改为(ref 并匹配关闭的&gt;,似乎我应该能够将树编译到我的可执行文件中。为什么这不是微不足道的?
  • 在 clojure 阅读器类型是“可读”或“不可读”,这取决于它们是否可以写入文件并读回。这是读者使用的术语,而不是对序列化和解析它们的可能性的看法。
  • 明白,谢谢。我现在正在做的是将数据存储在文本文件中并读入,不幸的是,这使我的应用程序在命令行中无用,需要成为守护程序/服务器......我一般虽然我想知道正确的策略是什么用于在已编译的 java 程序中包含大型数据结构......我也已经通过编码遇到了方法大小的 64k 限制。
  • 如果您可以将其构建为单个 ref 中的树而不是多个 ref 的树,那么生活真的会容易得多。当然,这可能无法直接实现,但如果您可以使用此表单来存储它,然后在加载后转换回工作表单可能会容易得多。
【解决方案2】:

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

【讨论】:

  • 是的,这是一次制作,然后以只读方式使用的东西。我想知道,我可以将序列化的对象编译到目标jar中,然后在运行java -jar treeLookup.jar arg1 arg2时加载它吗?
  • 是的,您可以将序列化的对象包含在目标 jar 中,并在需要时或在初始化类时对其进行反序列化。我同意@arthurulfeldt 的观点,即没有所有裁判,生活会更轻松。
  • 嘿,你能指点我在类初始化时如何加载它的参考吗?
  • 使用类变量:static final STRUCTURE = methodWhichLoadsStructure()
【解决方案3】:

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

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

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

当然,这是文字的编译方式,因为上面的宏是一个 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,并将结果嵌入到编译代码中(在某种意义上,包括适当的常量和代码以在类文件中重建数据)。在运行时,不会执行进一步的读取。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-19
    • 1970-01-01
    • 2019-01-31
    • 2015-03-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多