如何理解 Tree 或 Trie 中的浅层大小和保留大小?

Posted

技术标签:

【中文标题】如何理解 Tree 或 Trie 中的浅层大小和保留大小?【英文标题】:How to understand shallow size and retain size in a Tree or Trie? 【发布时间】:2021-02-15 13:39:29 【问题描述】:

我有一个字符 trie 数据结构,如下所示:

sealed class TrieNode(val children: MutableMap<Char, TrieNode>) 
    class NormalNode(kids: MutableMap<Char, TrieNode>) : TrieNode(kids)
    class EndNode(kids: MutableMap<Char, TrieNode>, val info: SubInfo) : TrieNode(kids)

如您所见,我的 trie 由 NormalNodeEndNode 组成,其中 NormalNodes 是内部节点,EndNodes 是一次叶子。

当我在运行时创建 trie 后进行内存分析时,我可以看到 TrieNode 类的浅层内存使用量为1 MB,而保留使用量为120 MB。代码在android中运行,实现似乎没有任何bug。

我的问题是类的保留内存在这种嵌套/复合实现中是否有意义。浅尺寸是对象本身的尺寸。但保留大小是所有私有引用(仅通过此路径访问的引用)及其子引用的总大小。现在考虑由相同类型的对象组成的树/树。每个节点的浅层大小将是节点的大小,但保留大小将是节点的大小 + 其所有子节点的大小之和,因为它的所有子节点只能通过这个父节点访问?

【问题讨论】:

【参考方案1】:

(我认为对此没有一个明确的答案,但这里有一些想法。)

正如您所说,“浅”大小应仅包括 TrieNode 对象本身,而不是它们所引用的任何内容。并且“保留”大小通常包括这些类可到达的任何内容:这将包括它们的children 映射以及可从它们到达的所有其余 trie;还有他们引用的info 对象(以及他们引用的任何东西……)。

我最关心的是为每个节点使用Map 对象,因为大多数Map 实现会占用相当多的内存——而且您将为 trie 的每个节点创建一个。

最常见的映射类型是哈希映射,它为哈希表保存一个数组(可能从 16 个或更多条目开始,并且会根据需要增长到明显大于条目数);取决于它的实现方式,每个非空的哈希条目可能指向一个节点的链表,每个节点都可能引用键和相应的值。这是相当少的对象,占用的内存比您预期的要多。 (ConcurrentHashMap 以使用更多内存为代价获得了良好的并发性能。)

所以我怀疑这将解释浅层和深层内存使用之间的很多差异。 (当然,你的 trie 中的 info 对象也会包含在其中,所以如果它们非常大或最终涉及很多东西,那么地图可能不是主要因素。)

因此,如果内存使用是一个真正的问题,您可能需要重新考虑重写您的类以使用不同形式的存储。 (这可能需要付出很多努力,并使其更加复杂和/或不那么普遍,因此只有在这是一个重大问题时才值得这样做。)

例如,如果您希望每个节点的子节点数量非常少,那么您可以将 Map 替换为键和值的并行数组 - 大多数操作将是 O(n),但如果 n 总是很小那么这可能会被节省的内存所抵消。和/或如果您知道您的密钥始终是Chars,那么您可以对该类型进行硬编码并避免所有装箱的原语。

(作为中途之家,您创建了一个新的 MutableMap 实现,它具有一些内存优势;然后您可以保持您的干净的 trie 实现。)

【讨论】:

感谢您的回答,但我们确定分析器中显示的保留内存值重复计算了吗? 哦,不,任何合理的分析器都不会加倍计算任何东西。不确定的主要领域是它是否计算任何带有 other 引用的内容,因此在您的 trie 被删除后必须保留在内存中。但这可能不会在这里发生;如前所述,一旦你计算了所有地图和它们的内部对象,以及你的info 对象,我一点也不惊讶发现所有这些都需要几个数量级空间比你简单的小 TrieNode 类本身(每个只包含一个或两个对象引用)。​​

以上是关于如何理解 Tree 或 Trie 中的浅层大小和保留大小?的主要内容,如果未能解决你的问题,请参考以下文章

[LeetCode] 208. Implement Trie (Prefix Tree) 实现字典树(前缀树)

[可持久化Trie][HDU4757] Tree

TCP/IP具体解释学习笔记--TCP的坚持和保活定时器

Implement Trie (Prefix Tree)

Trie树的java实现

Trie (Prefix Tree)前缀树