假设有一个函数,它收到一个包含大约100KB数据的json字符串。该字符串转换为一个映射,然后它继续将新键与该映射关联,并替换let绑定中的var,如下所示:
(defn myfn
[str]
(let [j (json/read-str str)
j (assoc-in j [:key1 :subkey1] somedata1)
j (assoc-in j [:key2 :subkey2] somedata2)
....
j (assoc-in j [:key100 :subkey100] somedata100)]
... do something ...))
我知道,在所有这些绑定之后,j将添加所有这些新键。这只是一个例子。我想知道在那些大量的绑定到同一个var中会发生什么。
我的意思是记忆中会发生什么。它会在内存中100次复制100KB吗?它会消耗100KB * 100 = 10,000KB直到它退出该功能?或者,Clojure足够聪明,它实际上在同一个内存空间中不断添加新密钥?
如果您还可以在Clojure参考中找到我应该寻找的地方,以找到答案,那将是非常好的。
答案 0 :(得分:2)
Clojure使用一种名为trie
的数据结构,它类似于树,但只在叶节点上有数据。大多数clojure的持久性结构都是以trie的形式实现的。
This excellent article确实详细解释了事情并使用了向量,所以我不会在这里重复它。我知道S.O.它更倾向于提供内容而不是链接,但它不是一个可以在答案中完全涵盖的主题,而且文章做得最好,所以我只是链接到它
简而言之,当以某种方式修改数据结构时,会为新的"版本"创建一个新的trie,而不是通过一次更改将所有数据从旧的复制到新的#34;在新的结构中,节点指向现有数据。以下是显示数据共享的上述文章的可视化:
所以,使用这种结构,我们有共享数据,但由于它只是一个二进制trie,它可以很快变深,所以查找可能需要很长时间(对于10亿个元素的向量,深度为到达叶子节点的是log 2 1e9,即30)。为了解决这个问题,clojure使用32路分支因子而不是2路分支因子,产生非常浅的树。因此,在clojure中保存10亿个元素的相同向量只需要log 32 1e9或6个间接级别来到达叶子。
我建议您阅读该文章,并查看PersistentHashMap
,您会在多个地方看到对shift + 5
的引用。这是一种巧妙的方法,可以使用位移来索引到trie(log 2 32 = 5)。有关详细信息,请参阅second part of the article。
总而言之,clojure使用高效的数据结构来实现持久性,任何具有不可变性作为核心功能的语言都必须这样做,如果它希望实现可用的性能。